Merge branch 'coffee-rewrite'

Conflicts:
	morris.js
This commit is contained in:
Olly Smith 2012-02-25 18:04:26 +00:00
commit 9b4791534b
3 changed files with 602 additions and 292 deletions

View File

@ -45,7 +45,7 @@
{"period": "1995 Q4", "licensed": 1702, "sorned": 0},
{"period": "1994 Q4", "licensed": 1732, "sorned": 0}
];
$('#graph').hml({
new Morris.Line("graph", {
data: tax_data,
xkey: 'period',
ykeys: ['licensed', 'sorned'],
@ -53,4 +53,4 @@
});
})
</script>
</body>
</body>

298
morris.coffee Normal file
View File

@ -0,0 +1,298 @@
# The original line graph.
#
window.Morris = {}
class window.Morris.Line
# Initialise the graph.
#
# @param {string} id Target element's DOM ID
# @param {Object} options
constructor: (id, options) ->
@el = $ document.getElementById(id)
@options = $.extend @defaults, options
# bail if there's no data
if @options.data is undefined or @options.data.length is 0
return
@el.addClass 'graph-initialised'
@precalc()
@redraw()
# Default configuration
#
defaults:
lineWidth: 3
pointSize: 4
lineColors: [
'#0b62a4'
'#7A92A3'
'#4da74d'
'#afd8f8'
'#edc240'
'#cb4b4b'
'#9440ed'
]
marginTop: 25
marginRight: 25
marginBottom: 30
marginLeft: 25
numLines: 5
gridLineColor: '#aaa'
gridTextColor: '#888'
gridTextSize: 12
gridStrokeWidth: 0.5
hoverPaddingX: 10
hoverPaddingY: 5
hoverMargin: 10
hoverFillColor: '#fff'
hoverBorderColor: '#ccc'
hoverBorderWidth: 2
hoverOpacity: 0.95
hoverLabelColor: '#444'
hoverFontSize: 12
# Do any necessary pre-processing for a new dataset
#
precalc: ->
# extract labels
@columnLabels = $.map @options.data, (d) => d[@options.xkey]
@seriesLabels = @options.labels
# extract series data
@series = []
for ykey in @options.ykeys
@series.push $.map @options.data, (d) -> d[ykey]
# translate x labels into nominal dates
# note: currently using decimal years to specify dates
@xvals = $.map @columnLabels, (x) => @parseYear x
@xmin = Math.min.apply null, @xvals
@xmax = Math.max.apply null, @xvals
if @xmin is @xmax
@xmin -= 1
@xmax += 1
# use $.map to flatten arrays and find the max y value
all_y_vals = $.map @series, (x) -> Math.max.apply null, x
@ymax = Math.max(20, Math.max.apply(null, all_y_vals))
# Clear and redraw the graph
#
redraw: ->
# remove child elements (get rid of old drawings)
@el.empty()
# the raphael drawing instance
@r = new Raphael(@el[0])
# calculate grid dimensions
left = @measureText(@ymax, @options.gridTextSize).width + @options.marginLeft
width = @el.width() - left - @options.marginRight
height = @el.height() - @options.marginTop - @options.marginBottom
dx = width / (@xmax - @xmin)
dy = height / @ymax
# quick translation helpers
transX = (x) =>
if @xvals.length is 1
left + width / 2
else
left + (x - @xmin) * dx
transY = (y) =>
return @options.marginTop + height - y * dy
# draw y axis labels, horizontal lines
lineInterval = height / (@options.numLines - 1)
for i in [0..@options.numLines-1]
y = @options.marginTop + i * lineInterval
v = Math.round((@options.numLines - 1 - i) * @ymax / (@options.numLines - 1))
@r.text(left - @options.marginLeft/2, y, v)
.attr('font-size', @options.gridTextSize)
.attr('fill', @options.gridTextColor)
.attr('text-anchor', 'end')
@r.path("M" + left + "," + y + 'H' + (left + width))
.attr('stroke', @options.gridLineColor)
.attr('stroke-width', @options.gridStrokeWidth)
# draw x axis labels
prevLabelMargin = null
xLabelMargin = 50 # make this an option?
for i in [Math.ceil(@xmin)..Math.floor(@xmax)]
label = @r.text(transX(i), @options.marginTop + height + @options.marginBottom / 2, i)
.attr('font-size', @options.gridTextSize)
.attr('fill', @options.gridTextColor)
labelBox = label.getBBox()
# ensure a minimum of `xLabelMargin` pixels between labels
if prevLabelMargin is null or prevLabelMargin <= labelBox.x
prevLabelMargin = labelBox.x + labelBox.width + xLabelMargin
else
label.remove()
# draw the actual series
columns = (transX(x) for x in @xvals)
seriesCoords = []
for s in @series
seriesCoords.push($.map(s, (y, i) -> x: columns[i], y: transY(y)))
for i in [seriesCoords.length-1..0]
coords = seriesCoords[i]
if coords.length > 1
path = @createPath coords, @options.marginTop, left, @options.marginTop + height, left + width
@r.path(path)
.attr('stroke', @options.lineColors[i])
.attr('stroke-width', @options.lineWidth)
seriesPoints = ([] for i in [0..seriesCoords.length-1])
for i in [seriesCoords.length-1..0]
for c in seriesCoords[i]
circle = @r.circle(c.x, c.y, @options.pointSize)
.attr('fill', @options.lineColors[i])
.attr('stroke-width', 1)
.attr('stroke', '#ffffff')
seriesPoints[i].push(circle)
# hover labels
hoverHeight = @options.hoverFontSize * 1.5 * (@series.length + 1)
hover = @r.rect(-10, -hoverHeight / 2 - @options.hoverPaddingY, 20, hoverHeight + @options.hoverPaddingY * 2, 10)
.attr('fill', @options.hoverFillColor)
.attr('stroke', @options.hoverBorderColor)
.attr('stroke-width', @options.hoverBorderWidth)
.attr('opacity', @options.hoverOpacity)
xLabel = @r.text(0, (@options.hoverFontSize * 0.75) - hoverHeight / 2, '')
.attr('fill', @options.hoverLabelColor)
.attr('font-weight', 'bold')
.attr('font-size', @options.hoverFontSize)
hoverSet = @r.set()
hoverSet.push(hover)
hoverSet.push(xLabel)
yLabels = []
for i in [0..@series.length-1]
yLabel = @r.text(0, @options.hoverFontSize * 1.5 * (i + 1.5) - hoverHeight / 2, '')
.attr('fill', @options.lineColors[i])
.attr('font-size', @options.hoverFontSize)
yLabels.push(yLabel)
hoverSet.push(yLabel)
updateHover = (index) =>
hoverSet.show()
xLabel.attr('text', @columnLabels[index])
for i in [0..@series.length-1]
yLabels[i].attr('text', "#{@seriesLabels[i]}: #{@commas(@series[i][index])}")
# recalculate hover box width
maxLabelWidth = Math.max.apply null, $.map yLabels, (l) ->
l.getBBox().width
maxLabelWidth = Math.max maxLabelWidth, xLabel.getBBox().width
hover.attr 'width', maxLabelWidth + @options.hoverPaddingX * 2
hover.attr 'x', -@options.hoverPaddingX - maxLabelWidth / 2
# move to y pos
yloc = Math.min.apply null, $.map @series, (s) =>
transY s[index]
if yloc > hoverHeight + @options.hoverPaddingY * 2 + @options.hoverMargin + @options.marginTop
yloc = yloc - hoverHeight / 2 - @options.hoverPaddingY - @options.hoverMargin
else
yloc = yloc + hoverHeight / 2 + @options.hoverPaddingY + @options.hoverMargin
yloc = Math.max @options.marginTop + hoverHeight / 2 + @options.hoverPaddingY, yloc
yloc = Math.min @options.marginTop + height - hoverHeight / 2 - @options.hoverPaddingY, yloc
xloc = Math.min left + width - maxLabelWidth / 2 - @options.hoverPaddingX, columns[index]
xloc = Math.max left + maxLabelWidth / 2 + @options.hoverPaddingX, xloc
hoverSet.attr 'transform', "t#{xloc},#{yloc}"
hideHover = ->
hoverSet.hide()
# column hilight
hoverMargins = $.map columns.slice(1), (x, i) -> (x + columns[i]) / 2
prevHilight = null
pointGrow = Raphael.animation r: @options.pointSize + 3, 25, 'linear'
pointShrink = Raphael.animation r: @options.pointSize, 25, 'linear'
hilight = (index) =>
if prevHilight isnt null and prevHilight isnt index
for i in [0..seriesPoints.length-1]
seriesPoints[i][prevHilight].animate pointShrink
if index isnt null and prevHilight isnt index
for i in [0..seriesPoints.length-1]
seriesPoints[i][index].animate pointGrow
updateHover index
prevHilight = index
if index is null
hideHover()
updateHilight = (x) =>
x -= @el.offset().left
for i in [hoverMargins.length..1]
if hoverMargins[i - 1] > x
break
hilight i
@el.mousemove (evt) =>
updateHilight evt.pageX
touchHandler = (evt) =>
touch = evt.originalEvent.touches[0] or evt.originalEvent.changedTouches[0]
updateHilight touch.pageX
return touch
@el.bind 'touchstart', touchHandler
@el.bind 'touchmove', touchHandler
@el.bind 'touchend', touchHandler
hilight 0
# create a path for a data series
#
createPath: (coords, top, left, bottom, right) ->
path = ""
grads = @gradients coords
for i in [0..coords.length-1]
c = coords[i]
if i is 0
path += "M#{c.x},#{c.y}"
else
g = grads[i]
lc = coords[i - 1]
lg = grads[i - 1]
ix = (c.x - lc.x) / 4
x1 = lc.x + ix
y1 = Math.min(bottom, lc.y + ix * lg)
x2 = c.x - ix
y2 = Math.min(bottom, c.y - ix * g)
path += "C#{x1},#{y1},#{x2},#{y2},#{c.x},#{c.y}"
return path
# calculate a gradient at each point for a series of points
#
gradients: (coords) ->
$.map coords, (c, i) ->
if i is 0
(coords[1].y - c.y) / (coords[1].x - c.x)
else if i is (coords.length - 1)
(c.y - coords[i - 1].y) / (c.x - coords[i - 1].x)
else
(coords[i + 1].y - coords[i - 1].y) / (coords[i + 1].x - coords[i - 1].x)
measureText: (text, fontSize = 12) ->
tt = @r.text(100, 100, text).attr('font-size', fontSize)
ret = tt.getBBox()
tt.remove()
return ret
parseYear: (date) ->
s = date.toString()
m = s.match /^(\d+) Q(\d)$/
n = s.match /^(\d+)-(\d+)$/
o = s.match /^(\d+)-(\d+)-(\d+)$/
if m
parseInt(m[1], 10) + (parseInt(m[2], 10) * 3 - 1) / 12
else if n
parseInt(n[1], 10) + (parseInt(n[2], 10) - 1) / 12
else if o
# parse to a timestamp
year = parseInt(o[1], 10);
month = parseInt(o[2], 10);
day = parseInt(o[3], 10);
timestamp = new Date(year, month - 1, day).getTime();
# get timestamps for the beginning and end of the year
y1 = new Date(year, 0, 1).getTime();
y2 = new Date(year+1, 0, 1).getTime();
# calculate a decimal-year value
year + (timestamp - y1) / (y2 - y1);
else
parseInt(d, 10)
# make long numbers prettier by inserting commas
# eg: commas(1234567) -> '1,234,567'
#
commas: (num) ->
Math.max(0, num).toFixed(0).replace(/(?=(?:\d{3})+$)(?!^)/g, ',')
# vim: set et ts=2 sw=2 sts=2

592
morris.js
View File

@ -1,306 +1,318 @@
(function() {
/*global jQuery: false, Raphael: false */
window.Morris = {};
function parse_year(date) {
var m = date.toString().match(/^(\d+) Q(\d)$/);
var n = date.toString().match(/^(\d+)-(\d+)$/);
var o = date.toString().match(/^(\d+)-(\d+)-(\d+)$/)
if (m) {
return parseInt(m[1], 10) + (parseInt(m[2], 10) * 3 - 1) / 12;
}
else if (n) {
return parseInt(n[1], 10) + (parseInt(n[2], 10) - 1) / 12;
}
else if (o) {
// parse to a timestamp
var year = parseInt(o[1], 10);
var month = parseInt(o[2], 10);
var day = parseInt(o[3], 10);
var timestamp = new Date(year, month - 1, day).getTime();
// get timestamps for the beginning and end of the year
var y1 = new Date(year, 0, 1).getTime();
var y2 = new Date(year+1, 0, 1).getTime();
// calculate a decimal-year value
return year + (timestamp - y1) / (y2 - y1);
}
else {
return parseInt(date, 10);
}
}
window.Morris.Line = (function() {
function setup_graph(config) {
/*jshint loopfunc: true */
var data = config.data;
if (data.length === 0) {
return;
}
this.addClass('graph-initialised');
var xlabels = $.map(data, function (d) { return d[config.xkey]; });
var series = config.ykeys;
var labels = config.labels;
if (!data || !data.length) {
return;
}
for (var i = 0; i < series.length; i++) {
series[i] = $.map(data, function (d) { return d[series[i]]; });
}
var xvals = $.map(xlabels, function (x) { return parse_year(x); });
var xmin = Math.min.apply(null, xvals);
var xmax = Math.max.apply(null, xvals);
if (xmin === xmax) {
xmin -= 1;
xmax += 1;
}
var ymax = Math.max(20, Math.max.apply(null,
$.map(series, function (s) { return Math.max.apply(null, s); })));
var r = new Raphael(this[0]);
var margin_top = 25, margin_bottom = 30, margin_right = 25;
var tt = r.text(100, 100, ymax).attr('font-size', 12);
var margin_left = 25 + tt.getBBox().width;
tt.remove();
var h = this.height() - margin_top - margin_bottom;
var w = this.width() - margin_left - margin_right;
var dx = w / (xmax - xmin);
var dy = h / ymax;
function trans_x(x) {
if (xvals.length === 1) {
return margin_left + w / 2;
function Line(id, options) {
this.el = $(document.getElementById(id));
this.options = $.extend(this.defaults, options);
if (this.options.data === void 0 || this.options.data.length === 0) return;
this.el.addClass('graph-initialised');
this.precalc();
this.redraw();
}
else {
return margin_left + (x - xmin) * dx;
}
}
function trans_y(y) {
return margin_top + h - y * dy;
}
// draw horizontal lines
var num_lines = 5;
var line_interval = h / (num_lines - 1);
for (i = 0; i < num_lines; i++) {
var y = margin_top + i * line_interval;
r.text(margin_left - 12, y, Math.floor((num_lines - 1 - i) * ymax / (num_lines - 1)))
.attr('font-size', 12)
.attr('fill', '#888')
.attr('text-anchor', 'end');
r.path("M" + (margin_left) + "," + y + "L" + (margin_left + w) + "," + y)
.attr('stroke', '#aaa')
.attr('stroke-width', 0.5);
}
// calculate the columns
var cols = $.map(xvals, trans_x);
var hover_margins = $.map(cols.slice(1),
function (x, i) { return (x + cols[i]) / 2; });
Line.prototype.defaults = {
lineWidth: 3,
pointSize: 4,
lineColors: ['#0b62a4', '#7A92A3', '#4da74d', '#afd8f8', '#edc240', '#cb4b4b', '#9440ed'],
marginTop: 25,
marginRight: 25,
marginBottom: 30,
marginLeft: 25,
numLines: 5,
gridLineColor: '#aaa',
gridTextColor: '#888',
gridTextSize: 12,
gridStrokeWidth: 0.5,
hoverPaddingX: 10,
hoverPaddingY: 5,
hoverMargin: 10,
hoverFillColor: '#fff',
hoverBorderColor: '#ccc',
hoverBorderWidth: 2,
hoverOpacity: 0.95,
hoverLabelColor: '#444',
hoverFontSize: 12
};
var last_label = null;
var ylabel_margin = 50;
for (i = Math.ceil(xmin); i <= Math.floor(xmax); i++) {
var label = r.text(trans_x(i), margin_top + h + margin_bottom / 2, i)
.attr('font-size', 12)
.attr('fill', '#888');
if (last_label !== null) {
var bb1 = last_label.getBBox();
var bb2 = label.getBBox();
if (bb1.x + bb1.width + ylabel_margin > bb2.x) {
label.remove();
Line.prototype.precalc = function() {
var all_y_vals, ykey, _i, _len, _ref,
_this = this;
this.columnLabels = $.map(this.options.data, function(d) {
return d[_this.options.xkey];
});
this.seriesLabels = this.options.labels;
this.series = [];
_ref = this.options.ykeys;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
ykey = _ref[_i];
this.series.push($.map(this.options.data, function(d) {
return d[ykey];
}));
}
else {
last_label = label;
this.xvals = $.map(this.columnLabels, function(x) {
return _this.parseYear(x);
});
this.xmin = Math.min.apply(null, this.xvals);
this.xmax = Math.max.apply(null, this.xvals);
if (this.xmin === this.xmax) {
this.xmin -= 1;
this.xmax += 1;
}
}
else {
last_label = label;
}
}
all_y_vals = $.map(this.series, function(x) {
return Math.max.apply(null, x);
});
return this.ymax = Math.max(20, Math.max.apply(null, all_y_vals));
};
// draw the series
var series_points = [];
for (var s = (series.length - 1); s >= 0; s--) {
var path = '';
var lc = null;
var lg = null;
// translate the coordinates into screen positions
var coords = $.map(series[s],
function (v, idx) { return {x: cols[idx], y: trans_y(v)}; });
if (coords.length > 1) {
// calculate the gradients
var grads = $.map(coords, function (c, i) {
Line.prototype.redraw = function() {
var c, circle, columns, coords, dx, dy, height, hideHover, hilight, hover, hoverHeight, hoverMargins, hoverSet, i, label, labelBox, left, lineInterval, path, pointGrow, pointShrink, prevHilight, prevLabelMargin, s, seriesCoords, seriesPoints, touchHandler, transX, transY, updateHilight, updateHover, v, width, x, xLabel, xLabelMargin, y, yLabel, yLabels, _i, _j, _len, _len2, _ref, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8,
_this = this;
this.el.empty();
this.r = new Raphael(this.el[0]);
left = this.measureText(this.ymax, this.options.gridTextSize).width + this.options.marginLeft;
width = this.el.width() - left - this.options.marginRight;
height = this.el.height() - this.options.marginTop - this.options.marginBottom;
dx = width / (this.xmax - this.xmin);
dy = height / this.ymax;
transX = function(x) {
if (_this.xvals.length === 1) {
return left + width / 2;
} else {
return left + (x - _this.xmin) * dx;
}
};
transY = function(y) {
return _this.options.marginTop + height - y * dy;
};
lineInterval = height / (this.options.numLines - 1);
for (i = 0, _ref = this.options.numLines - 1; 0 <= _ref ? i <= _ref : i >= _ref; 0 <= _ref ? i++ : i--) {
y = this.options.marginTop + i * lineInterval;
v = Math.round((this.options.numLines - 1 - i) * this.ymax / (this.options.numLines - 1));
this.r.text(left - this.options.marginLeft / 2, y, v).attr('font-size', this.options.gridTextSize).attr('fill', this.options.gridTextColor).attr('text-anchor', 'end');
this.r.path("M" + left + "," + y + 'H' + (left + width)).attr('stroke', this.options.gridLineColor).attr('stroke-width', this.options.gridStrokeWidth);
}
prevLabelMargin = null;
xLabelMargin = 50;
for (i = _ref2 = Math.ceil(this.xmin), _ref3 = Math.floor(this.xmax); _ref2 <= _ref3 ? i <= _ref3 : i >= _ref3; _ref2 <= _ref3 ? i++ : i--) {
label = this.r.text(transX(i), this.options.marginTop + height + this.options.marginBottom / 2, i).attr('font-size', this.options.gridTextSize).attr('fill', this.options.gridTextColor);
labelBox = label.getBBox();
if (prevLabelMargin === null || prevLabelMargin <= labelBox.x) {
prevLabelMargin = labelBox.x + labelBox.width + xLabelMargin;
} else {
label.remove();
}
}
columns = (function() {
var _i, _len, _ref4, _results;
_ref4 = this.xvals;
_results = [];
for (_i = 0, _len = _ref4.length; _i < _len; _i++) {
x = _ref4[_i];
_results.push(transX(x));
}
return _results;
}).call(this);
seriesCoords = [];
_ref4 = this.series;
for (_i = 0, _len = _ref4.length; _i < _len; _i++) {
s = _ref4[_i];
seriesCoords.push($.map(s, function(y, i) {
return {
x: columns[i],
y: transY(y)
};
}));
}
for (i = _ref5 = seriesCoords.length - 1; _ref5 <= 0 ? i <= 0 : i >= 0; _ref5 <= 0 ? i++ : i--) {
coords = seriesCoords[i];
if (coords.length > 1) {
path = this.createPath(coords, this.options.marginTop, left, this.options.marginTop + height, left + width);
this.r.path(path).attr('stroke', this.options.lineColors[i]).attr('stroke-width', this.options.lineWidth);
}
}
seriesPoints = (function() {
var _ref6, _results;
_results = [];
for (i = 0, _ref6 = seriesCoords.length - 1; 0 <= _ref6 ? i <= _ref6 : i >= _ref6; 0 <= _ref6 ? i++ : i--) {
_results.push([]);
}
return _results;
})();
for (i = _ref6 = seriesCoords.length - 1; _ref6 <= 0 ? i <= 0 : i >= 0; _ref6 <= 0 ? i++ : i--) {
_ref7 = seriesCoords[i];
for (_j = 0, _len2 = _ref7.length; _j < _len2; _j++) {
c = _ref7[_j];
circle = this.r.circle(c.x, c.y, this.options.pointSize).attr('fill', this.options.lineColors[i]).attr('stroke-width', 1).attr('stroke', '#ffffff');
seriesPoints[i].push(circle);
}
}
hoverHeight = this.options.hoverFontSize * 1.5 * (this.series.length + 1);
hover = this.r.rect(-10, -hoverHeight / 2 - this.options.hoverPaddingY, 20, hoverHeight + this.options.hoverPaddingY * 2, 10).attr('fill', this.options.hoverFillColor).attr('stroke', this.options.hoverBorderColor).attr('stroke-width', this.options.hoverBorderWidth).attr('opacity', this.options.hoverOpacity);
xLabel = this.r.text(0, (this.options.hoverFontSize * 0.75) - hoverHeight / 2, '').attr('fill', this.options.hoverLabelColor).attr('font-weight', 'bold').attr('font-size', this.options.hoverFontSize);
hoverSet = this.r.set();
hoverSet.push(hover);
hoverSet.push(xLabel);
yLabels = [];
for (i = 0, _ref8 = this.series.length - 1; 0 <= _ref8 ? i <= _ref8 : i >= _ref8; 0 <= _ref8 ? i++ : i--) {
yLabel = this.r.text(0, this.options.hoverFontSize * 1.5 * (i + 1.5) - hoverHeight / 2, '').attr('fill', this.options.lineColors[i]).attr('font-size', this.options.hoverFontSize);
yLabels.push(yLabel);
hoverSet.push(yLabel);
}
updateHover = function(index) {
var i, maxLabelWidth, xloc, yloc, _ref9;
hoverSet.show();
xLabel.attr('text', _this.columnLabels[index]);
for (i = 0, _ref9 = _this.series.length - 1; 0 <= _ref9 ? i <= _ref9 : i >= _ref9; 0 <= _ref9 ? i++ : i--) {
yLabels[i].attr('text', "" + _this.seriesLabels[i] + ": " + (_this.commas(_this.series[i][index])));
}
maxLabelWidth = Math.max.apply(null, $.map(yLabels, function(l) {
return l.getBBox().width;
}));
maxLabelWidth = Math.max(maxLabelWidth, xLabel.getBBox().width);
hover.attr('width', maxLabelWidth + _this.options.hoverPaddingX * 2);
hover.attr('x', -_this.options.hoverPaddingX - maxLabelWidth / 2);
yloc = Math.min.apply(null, $.map(_this.series, function(s) {
return transY(s[index]);
}));
if (yloc > hoverHeight + _this.options.hoverPaddingY * 2 + _this.options.hoverMargin + _this.options.marginTop) {
yloc = yloc - hoverHeight / 2 - _this.options.hoverPaddingY - _this.options.hoverMargin;
} else {
yloc = yloc + hoverHeight / 2 + _this.options.hoverPaddingY + _this.options.hoverMargin;
}
yloc = Math.max(_this.options.marginTop + hoverHeight / 2 + _this.options.hoverPaddingY, yloc);
yloc = Math.min(_this.options.marginTop + height - hoverHeight / 2 - _this.options.hoverPaddingY, yloc);
xloc = Math.min(left + width - maxLabelWidth / 2 - _this.options.hoverPaddingX, columns[index]);
xloc = Math.max(left + maxLabelWidth / 2 + _this.options.hoverPaddingX, xloc);
return hoverSet.attr('transform', "t" + xloc + "," + yloc);
};
hideHover = function() {
return hoverSet.hide();
};
hoverMargins = $.map(columns.slice(1), function(x, i) {
return (x + columns[i]) / 2;
});
prevHilight = null;
pointGrow = Raphael.animation({
r: this.options.pointSize + 3
}, 25, 'linear');
pointShrink = Raphael.animation({
r: this.options.pointSize
}, 25, 'linear');
hilight = function(index) {
var i, _ref10, _ref9;
if (prevHilight !== null && prevHilight !== index) {
for (i = 0, _ref9 = seriesPoints.length - 1; 0 <= _ref9 ? i <= _ref9 : i >= _ref9; 0 <= _ref9 ? i++ : i--) {
seriesPoints[i][prevHilight].animate(pointShrink);
}
}
if (index !== null && prevHilight !== index) {
for (i = 0, _ref10 = seriesPoints.length - 1; 0 <= _ref10 ? i <= _ref10 : i >= _ref10; 0 <= _ref10 ? i++ : i--) {
seriesPoints[i][index].animate(pointGrow);
}
updateHover(index);
}
prevHilight = index;
if (index === null) return hideHover();
};
updateHilight = function(x) {
var i, _ref9;
x -= _this.el.offset().left;
for (i = _ref9 = hoverMargins.length; _ref9 <= 1 ? i <= 1 : i >= 1; _ref9 <= 1 ? i++ : i--) {
if (hoverMargins[i - 1] > x) break;
}
return hilight(i);
};
this.el.mousemove(function(evt) {
return updateHilight(evt.pageX);
});
touchHandler = function(evt) {
var touch;
touch = evt.originalEvent.touches[0] || evt.originalEvent.changedTouches[0];
updateHilight(touch.pageX);
return touch;
};
this.el.bind('touchstart', touchHandler);
this.el.bind('touchmove', touchHandler);
this.el.bind('touchend', touchHandler);
return hilight(0);
};
Line.prototype.createPath = function(coords, top, left, bottom, right) {
var c, g, grads, i, ix, lc, lg, path, x1, x2, y1, y2, _ref;
path = "";
grads = this.gradients(coords);
for (i = 0, _ref = coords.length - 1; 0 <= _ref ? i <= _ref : i >= _ref; 0 <= _ref ? i++ : i--) {
c = coords[i];
if (i === 0) {
path += "M" + c.x + "," + c.y;
} else {
g = grads[i];
lc = coords[i - 1];
lg = grads[i - 1];
ix = (c.x - lc.x) / 4;
x1 = lc.x + ix;
y1 = Math.min(bottom, lc.y + ix * lg);
x2 = c.x - ix;
y2 = Math.min(bottom, c.y - ix * g);
path += "C" + x1 + "," + y1 + "," + x2 + "," + y2 + "," + c.x + "," + c.y;
}
}
return path;
};
Line.prototype.gradients = function(coords) {
return $.map(coords, function(c, i) {
if (i === 0) {
return (coords[1].y - c.y) / (coords[1].x - c.x);
}
else if (i === xvals.length - 1) {
} else if (i === (coords.length - 1)) {
return (c.y - coords[i - 1].y) / (c.x - coords[i - 1].x);
}
else {
} else {
return (coords[i + 1].y - coords[i - 1].y) / (coords[i + 1].x - coords[i - 1].x);
}
});
for (i = 0; i < coords.length; i++) {
var c = coords[i];
var g = grads[i];
if (i === 0) {
path += "M" + ([c.x, c.y].join(','));
}
else {
var ix = (c.x - lc.x) / 4;
path += "C" + ([lc.x + ix,
Math.min(margin_top + h, lc.y + ix * lg),
c.x - ix,
Math.min(margin_top + h, c.y - ix * g),
c.x, c.y].join(','));
}
lc = c;
lg = g;
}
r.path(path)
.attr('stroke', config.line_colors[s])
.attr('stroke-width', config.line_width);
// draw the points
}
series_points.push([]);
for (i = 0; i < series[s].length; i++) {
var c1 = {x: cols[i], y: trans_y(series[s][i])};
var circle = r.circle(c1.x, c1.y, config.point_size)
.attr('fill', config.line_colors[s])
.attr('stroke-width', 1)
.attr('stroke', '#ffffff');
series_points[series_points.length - 1].push(circle);
}
}
// hover labels
var label_height = 12;
var label_padding_x = 10;
var label_padding_y = 5;
var label_margin = 10;
var yvar_labels = [];
var label_float_height = (label_height * 1.5) * (series.length + 1);
var label_float = r.rect(-10, -label_float_height / 2 - label_padding_y, 20, label_float_height + label_padding_y * 2, 10)
.attr('fill', '#fff')
.attr('stroke', '#ccc')
.attr('stroke-width', 2)
.attr('opacity', 0.95);
var xvar_label = r.text(0, (label_height * 0.75) - (label_float_height / 2), '')
.attr('fill', '#444')
.attr('font-weight', 'bold')
.attr('font-size', label_height);
var label_set = r.set();
label_set.push(label_float);
label_set.push(xvar_label);
for (i = 0; i < series.length; i++) {
var yl = r.text(0, (label_height * 1.5 * (i + 1.5)) - (label_float_height / 2), '')
.attr('fill', config.line_colors[i])
.attr('font-size', label_height);
yvar_labels.push(yl);
label_set.push(yl);
}
function commas(v) {
v = v.toString();
var r = "";
while (v.length > 3) {
r = "," + v.substr(v.length - 3) + r;
v = v.substr(0, v.length - 3);
}
r = v + r;
return r;
}
function update_float(index) {
label_set.show();
xvar_label.attr('text', xlabels[index]);
for (var i = 0; i < series.length; i++) {
yvar_labels[i].attr('text', labels[i] + ': ' + commas(series[i][index]));
}
// calculate bbox width
var bbw = Math.max(xvar_label.getBBox().width,
Math.max.apply(null, $.map(yvar_labels, function (l) { return l.getBBox().width; })));
label_float.attr('width', bbw + label_padding_x * 2);
label_float.attr('x', -label_padding_x - bbw / 2);
// determine y-pos
var yloc = Math.min.apply(null, $.map(series, function (s) { return trans_y(s[index]); }));
if (yloc > label_float_height + label_padding_y * 2 + label_margin + margin_top) {
yloc = yloc - label_float_height / 2 - label_padding_y - label_margin;
}
else {
yloc = yloc + label_float_height / 2 + label_padding_y + label_margin;
}
yloc = Math.max(margin_top + label_float_height / 2 + label_padding_y, yloc);
yloc = Math.min(margin_top + h - label_float_height / 2 - label_padding_y, yloc);
var xloc = Math.min(margin_left + w - bbw / 2 - label_padding_y, cols[index]);
xloc = Math.max(margin_left + bbw / 2 + label_padding_x, xloc);
label_set.attr('transform', 't' + xloc + ',' + yloc);
}
function hide_float() {
label_set.hide();
}
// column hilighting
var self = this;
var prev_hilight = null;
var point_grow = Raphael.animation({r: config.point_size + 3}, 25, "linear");
var point_shrink = Raphael.animation({r: config.point_size}, 25, "linear");
function highlight(index) {
var j;
if (prev_hilight !== null && prev_hilight !== index) {
for (j = 0; j < series_points.length; j++) {
series_points[j][prev_hilight].animate(point_shrink);
}
}
if (index !== null && prev_hilight !== index) {
for (j = 0; j < series_points.length; j++) {
series_points[j][index].animate(point_grow);
}
update_float(index);
}
prev_hilight = index;
if (index === null) {
hide_float();
}
}
function update_hilight(x_coord) {
var x = x_coord - self.offset().left;
for (var i = hover_margins.length; i > 0; i--) {
if (hover_margins[i - 1] > x) {
break;
}
}
highlight(i);
}
this.mousemove(function (evt) {
update_hilight(evt.pageX);
});
function touchhandler(evt) {
var touch = evt.originalEvent.touches[0] ||
evt.originalEvent.changedTouches[0];
update_hilight(touch.pageX);
return touch;
}
this.bind('touchstart', touchhandler);
this.bind('touchmove', touchhandler);
this.bind('touchend', touchhandler);
highlight(0);
}
};
$.fn.hml = function (options) {
var config = {
line_width: 3,
point_size: 4,
line_colors: [
'#0b62a4',
'#7A92A3',
'#4da74d',
'#afd8f8',
'#edc240',
'#cb4b4b',
'#9440ed'
]
};
if (options) {
$.extend(config, options);
}
return this.each(function () {
setup_graph.call($(this), config);
});
};
Line.prototype.measureText = function(text, fontSize) {
var ret, tt;
if (fontSize == null) fontSize = 12;
tt = this.r.text(100, 100, text).attr('font-size', fontSize);
ret = tt.getBBox();
tt.remove();
return ret;
};
Line.prototype.parseYear = function(date) {
var day, m, month, n, o, s, timestamp, y1, y2, year;
s = date.toString();
m = s.match(/^(\d+) Q(\d)$/);
n = s.match(/^(\d+)-(\d+)$/);
o = s.match(/^(\d+)-(\d+)-(\d+)$/);
if (m) {
return parseInt(m[1], 10) + (parseInt(m[2], 10) * 3 - 1) / 12;
} else if (n) {
return parseInt(n[1], 10) + (parseInt(n[2], 10) - 1) / 12;
} else if (o) {
year = parseInt(o[1], 10);
month = parseInt(o[2], 10);
day = parseInt(o[3], 10);
timestamp = new Date(year, month - 1, day).getTime();
y1 = new Date(year, 0, 1).getTime();
y2 = new Date(year + 1, 0, 1).getTime();
return year + (timestamp - y1) / (y2 - y1);
} else {
return parseInt(d, 10);
}
};
Line.prototype.commas = function(num) {
return Math.max(0, num).toFixed(0).replace(/(?=(?:\d{3})+$)(?!^)/g, ',');
};
return Line;
})();
}).call(this);