First post

This commit is contained in:
Olly Smith 2012-02-21 22:50:41 +00:00
commit eba644feca
3 changed files with 369 additions and 0 deletions

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# Morris.js - pretty time-series line graphs
Morris.js is the library that powers the graphs on http://howmanyleft.co.uk/.
This is a hugely pre-alpha release. It's a verbatim copy of the code fragment that's in use on *How Many Left*, so it's not really generically usable. Expect that to improve soon.
Cheers!
\- Olly
## Requirements
- [jQuery](http://jquery.com/) (>= 1.7 recommended, but it'll probably work with older versions)
- [Raphael.js](http://raphaeljs.com/) (>= 2.0)
## Usage
See `example.html`.
## Development
Very daring.
Fork, hack, send a pull request :)

56
example.html Normal file
View File

@ -0,0 +1,56 @@
<!doctype html>
<head>
<style>
#graph {
width: 600px;
height: 400px;
margin: 20px auto 0 auto;
}
</style>
</head>
<body>
<div id="graph"></div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script src="https://raw.github.com/DmitryBaranovskiy/raphael/300aa589f5a0ba7fce667cd62c7cdda0bd5ad904/raphael-min.js"></script>
<script src="morris.js"></script>
<script>
$(function () {
// data stolen from http://howmanyleft.co.uk/vehicle/jaguar_'e'_type
tax_data = [
{"period": "2011 Q3", "licensed": 3407, "sorned": 660},
{"period": "2011 Q2", "licensed": 3351, "sorned": 629},
{"period": "2011 Q1", "licensed": 3269, "sorned": 618},
{"period": "2010 Q4", "licensed": 3246, "sorned": 661},
{"period": "2010 Q3", "licensed": 3257, "sorned": 667},
{"period": "2010 Q2", "licensed": 3248, "sorned": 627},
{"period": "2010 Q1", "licensed": 3171, "sorned": 660},
{"period": "2009 Q4", "licensed": 3171, "sorned": 676},
{"period": "2009 Q3", "licensed": 3201, "sorned": 656},
{"period": "2009 Q2", "licensed": 3215, "sorned": 622},
{"period": "2009 Q1", "licensed": 3148, "sorned": 632},
{"period": "2008 Q4", "licensed": 3155, "sorned": 681},
{"period": "2008 Q3", "licensed": 3190, "sorned": 667},
{"period": "2007 Q4", "licensed": 3226, "sorned": 620},
{"period": "2006 Q4", "licensed": 3245, "sorned": 0},
{"period": "2005 Q4", "licensed": 3289, "sorned": 0},
{"period": "2004 Q4", "licensed": 3263, "sorned": 0},
{"period": "2003 Q4", "licensed": 3189, "sorned": 0},
{"period": "2002 Q4", "licensed": 3079, "sorned": 0},
{"period": "2001 Q4", "licensed": 3085, "sorned": 0},
{"period": "2000 Q4", "licensed": 3055, "sorned": 0},
{"period": "1999 Q4", "licensed": 3063, "sorned": 0},
{"period": "1998 Q4", "licensed": 2943, "sorned": 0},
{"period": "1997 Q4", "licensed": 2806, "sorned": 0},
{"period": "1996 Q4", "licensed": 2674, "sorned": 0},
{"period": "1995 Q4", "licensed": 1702, "sorned": 0},
{"period": "1994 Q4", "licensed": 1732, "sorned": 0}
];
$('#graph').hml({
data: tax_data,
xkey: 'period',
ykeys: ['licensed', 'sorned'],
labels: ['Licensed', 'SORN']
});
})
</script>
</body>

289
morris.js Normal file
View File

@ -0,0 +1,289 @@
/*global jQuery: false, Raphael: false */
function parse_year(year) {
var m = year.toString().match(/(\d+) Q(\d)/);
if (m) {
return parseInt(m[1], 10) + (parseInt(m[2], 19) * 3 - 1) / 12;
}
else {
return parseInt(year, 10);
}
}
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;
}
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; });
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();
}
else {
last_label = label;
}
}
else {
last_label = label;
}
}
// 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) {
if (i === 0) {
return (coords[1].y - c.y) / (coords[1].x - c.x);
}
else if (i === xvals.length - 1) {
return (c.y - coords[i - 1].y) / (c.x - coords[i - 1].x);
}
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);
});
};