mirror of
https://github.com/morrisjs/morris.js.git
synced 2024-11-10 21:36:34 +01:00
Merge branch 'coffee-rewrite'
Conflicts: morris.js
This commit is contained in:
commit
9b4791534b
@ -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
298
morris.coffee
Normal 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
592
morris.js
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user