From c920e8d25c17435f57e09a90db16a6a91b915da4 Mon Sep 17 00:00:00 2001 From: Edwin Woudt Date: Tue, 1 Jan 2013 13:56:10 +0100 Subject: [PATCH] Improve visualisation of graphs, with tooltips and select to zoom --- houseagent/templates/graph_create.html | 87 +- houseagent/templates/js/jquery.flot.js | 1416 +++++++++++------ .../templates/js/jquery.flot.selection.js | 344 ++++ .../templates/js/jquery.flot.tooltip.js | 12 + 4 files changed, 1351 insertions(+), 508 deletions(-) mode change 100644 => 100755 houseagent/templates/js/jquery.flot.js create mode 100755 houseagent/templates/js/jquery.flot.selection.js create mode 100644 houseagent/templates/js/jquery.flot.tooltip.js diff --git a/houseagent/templates/graph_create.html b/houseagent/templates/graph_create.html index 34185cb..4502a32 100644 --- a/houseagent/templates/graph_create.html +++ b/houseagent/templates/graph_create.html @@ -3,6 +3,8 @@ <%def name="head()"> + + @@ -20,73 +22,66 @@ function basic_data(d) { var value_name = $("#historic_values option:selected").text().split(' - ')[1]; - switch(value_name) - { - case "Temperature": - color = '#ff0000'; - break; - case "Humidity": - color = '#157DEC'; - break; - case "Light": case "Lux": - color = '#FFF380'; - break; - default: - color = "#CAEF90"; - } - - var data1 = [ { label: value_name, data: d, color: color} ]; + var data1 = [ { label: value_name, data: d, color: "#caef90"} ]; var options = { xaxis: { mode: "time", tickLength: 0 }, grid: { - borderColor: "#ffffff" + borderColor: "#ffffff", + hoverable: true + }, + tooltip: true, + tooltipOpts: { + content: "%x | %y", + xDateFormat: "%b %d %H:%M" }, series: { lines: { show: true, fill: true } - } + }, + selection: { + mode: "xy" + } }; - $.plot($("#graph_latest"), data1, options ); + var graph = $("#graph_latest"); + + graph.bind("plotselected", function (event, ranges) { + $.plot(graph, data1, $.extend(true, {}, options, { + xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to }, + yaxis: { min: ranges.yaxis.from, max: ranges.yaxis.to } + })); + }); + + $.plot(graph, data1, options ); } function agg_data(d) { var value_name = $("#historic_values option:selected").text().split(' - ')[1]; - switch(value_name) - { - case "Temperature": - color = '#ff0000'; - break; - case "Humidity": - color = '#157DEC'; - break; - case "Light": case "Lux": - color = '#FFF380'; - break; - default: - color = "#CAEF90"; - } - var data1 = [ {label: "current", data: d[0], color: "#caef90"}, {label: "min", data: d[1], color: "#1111da"}, - {label: "avg", data: d[2], color: "#da11da"}, - {label: "max", data: d[3], color: "#da1111"} + {label: "max", data: d[3], color: "#da1111"}, + {label: "avg", data: d[2], color: "#da11da"} ]; var options = { xaxis: { mode: "time", - tickSize: [30, "minute"], tickLength: 0 }, grid: { - borderColor: "#ffffff" + borderColor: "#ffffff", + hoverable: true + }, + tooltip: true, + tooltipOpts: { + content: "%s %x | %y", + xDateFormat: "%b %d %H:%M" }, series: { lines: { @@ -96,10 +91,22 @@ points: { show: true } - } + }, + selection: { + mode: "xy" + } }; - $.plot($("#graph_day"), data1, options ); + var graph = $("#graph_day"); + + graph.bind("plotselected", function (event, ranges) { + $.plot(graph, data1, $.extend(true, {}, options, { + xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to }, + yaxis: { min: ranges.yaxis.from, max: ranges.yaxis.to } + })); + }); + + $.plot(graph, data1, options ); } $('#create_button').click(function() { diff --git a/houseagent/templates/js/jquery.flot.js b/houseagent/templates/js/jquery.flot.js old mode 100644 new mode 100755 index 6534a46..aabc544 --- a/houseagent/templates/js/jquery.flot.js +++ b/houseagent/templates/js/jquery.flot.js @@ -1,4 +1,4 @@ -/* Javascript plotting library for jQuery, v. 0.6. +/*! Javascript plotting library for jQuery, v. 0.7. * * Released under the MIT license by IOLA, December 2007. * @@ -9,7 +9,7 @@ /* Plugin for jQuery for working with colors. * - * Version 1.0. + * Version 1.1. * * Inspiration from jQuery color animation plugin by John Resig. * @@ -22,10 +22,13 @@ * console.log(c.r, c.g, c.b, c.a); * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" * - * Note that .scale() and .add() work in-place instead of returning - * new objects. + * Note that .scale() and .add() return the same modified object + * instead of making a new one. + * + * V. 1.1: Fix error handling so e.g. parsing an empty string does + * produce a color rather than just crashing. */ -(function(){jQuery.color={};jQuery.color.make=function(E,D,B,C){var F={};F.r=E||0;F.g=D||0;F.b=B||0;F.a=C!=null?C:1;F.add=function(I,H){for(var G=0;G=1){return"rgb("+[F.r,F.g,F.b].join(",")+")"}else{return"rgba("+[F.r,F.g,F.b,F.a].join(",")+")"}};F.normalize=function(){function G(I,J,H){return JH?H:J)}F.r=G(0,parseInt(F.r),255);F.g=G(0,parseInt(F.g),255);F.b=G(0,parseInt(F.b),255);F.a=G(0,F.a,1);return F};F.clone=function(){return jQuery.color.make(F.r,F.b,F.g,F.a)};return F.normalize()};jQuery.color.extract=function(C,B){var D;do{D=C.css(B).toLowerCase();if(D!=""&&D!="transparent"){break}C=C.parent()}while(!jQuery.nodeName(C.get(0),"body"));if(D=="rgba(0, 0, 0, 0)"){D="transparent"}return jQuery.color.parse(D)};jQuery.color.parse=function(E){var D,B=jQuery.color.make;if(D=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10))}if(D=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10),parseFloat(D[4]))}if(D=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55)}if(D=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55,parseFloat(D[4]))}if(D=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(E)){return B(parseInt(D[1],16),parseInt(D[2],16),parseInt(D[3],16))}if(D=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(E)){return B(parseInt(D[1]+D[1],16),parseInt(D[2]+D[2],16),parseInt(D[3]+D[3],16))}var C=jQuery.trim(E).toLowerCase();if(C=="transparent"){return B(255,255,255,0)}else{D=A[C];return B(D[0],D[1],D[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(); +(function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return KI?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); // the actual Flot code (function($) { @@ -51,7 +54,11 @@ backgroundOpacity: 0.85 // set to 0 to avoid background }, xaxis: { + show: null, // null = auto-detect, true = always, false = never + position: "bottom", // or "top" mode: null, // null or "time" + color: null, // base color, labels, ticks + tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" transform: null, // null or f: number -> number to transform axis inverseTransform: null, // if transform is set, this should be the inverse function min: null, // min. value to show, null means set automatically @@ -61,6 +68,9 @@ tickFormatter: null, // fn: number -> string labelWidth: null, // size of tick labels in pixels labelHeight: null, + reserveSpace: null, // whether to reserve space even if axis isn't shown + tickLength: null, // size in pixels of ticks, or "full" for whole line + alignTicksWithAxis: null, // axis number or null for no sync // mode specific options tickDecimals: null, // no. of decimals, null means auto @@ -71,21 +81,19 @@ twelveHourClock: false // 12 or 24 time in time mode }, yaxis: { - autoscaleMargin: 0.02 - }, - x2axis: { - autoscaleMargin: null - }, - y2axis: { - autoscaleMargin: 0.02 + autoscaleMargin: 0.02, + position: "left" // or "right" }, + xaxes: [], + yaxes: [], series: { points: { show: false, radius: 3, lineWidth: 2, // in pixels fill: true, - fillColor: "#ffffff" + fillColor: "#ffffff", + symbol: "circle" // or callback }, lines: { // we don't put in show: false so we can see @@ -102,7 +110,7 @@ fill: true, fillColor: null, align: "left", // or "center" - horizontal: false // when horizontal, left is now top + horizontal: false }, shadowSize: 3 }, @@ -111,10 +119,12 @@ aboveData: false, color: "#545454", // primary color used for outline and labels backgroundColor: null, // null for transparent, else color - tickColor: "rgba(0,0,0,0.15)", // color used for the ticks + borderColor: null, // set if different from the grid color + tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" labelMargin: 5, // in pixels + axisMargin: 8, // in pixels borderWidth: 2, // in pixels - borderColor: null, // set if different from the grid color + minBorderMargin: null, // in pixels, null means taken from points radius markings: null, // array of ranges or fn: axes -> array of ranges markingsColor: "#f4f4f4", markingsLineWidth: 2, @@ -130,7 +140,7 @@ overlay = null, // canvas for interactive stuff on top of plot eventHolder = null, // jQuery object that events should be bound to ctx = null, octx = null, - axes = { xaxis: {}, yaxis: {}, x2axis: {}, y2axis: {} }, + xaxes = [], yaxes = [], plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, canvasWidth = 0, canvasHeight = 0, plotWidth = 0, plotHeight = 0, @@ -138,9 +148,11 @@ processOptions: [], processRawData: [], processDatapoints: [], + drawSeries: [], draw: [], bindEvents: [], - drawOverlay: [] + drawOverlay: [], + shutdown: [] }, plot = this; @@ -159,17 +171,35 @@ o.top += plotOffset.top; return o; }; - plot.getData = function() { return series; }; - plot.getAxes = function() { return axes; }; - plot.getOptions = function() { return options; }; + plot.getData = function () { return series; }; + plot.getAxes = function () { + var res = {}, i; + $.each(xaxes.concat(yaxes), function (_, axis) { + if (axis) + res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; + }); + return res; + }; + plot.getXAxes = function () { return xaxes; }; + plot.getYAxes = function () { return yaxes; }; + plot.c2p = canvasToAxisCoords; + plot.p2c = axisToCanvasCoords; + plot.getOptions = function () { return options; }; plot.highlight = highlight; plot.unhighlight = unhighlight; plot.triggerRedrawOverlay = triggerRedrawOverlay; plot.pointOffset = function(point) { - return { left: parseInt(axisSpecToRealAxis(point, "xaxis").p2c(+point.x) + plotOffset.left), - top: parseInt(axisSpecToRealAxis(point, "yaxis").p2c(+point.y) + plotOffset.top) }; + return { + left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left), + top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top) + }; + }; + plot.shutdown = shutdown; + plot.resize = function () { + getCanvasDimensions(); + resizeCanvas(canvas); + resizeCanvas(overlay); }; - // public attributes plot.hooks = hooks; @@ -177,7 +207,7 @@ // initialize initPlugins(plot); parseOptions(options_); - constructCanvas(); + setupCanvases(); setData(data_); setupGrid(); draw(); @@ -200,14 +230,45 @@ } function parseOptions(opts) { + var i; + $.extend(true, options, opts); + + if (options.xaxis.color == null) + options.xaxis.color = options.grid.color; + if (options.yaxis.color == null) + options.yaxis.color = options.grid.color; + + if (options.xaxis.tickColor == null) // backwards-compatibility + options.xaxis.tickColor = options.grid.tickColor; + if (options.yaxis.tickColor == null) // backwards-compatibility + options.yaxis.tickColor = options.grid.tickColor; + if (options.grid.borderColor == null) options.grid.borderColor = options.grid.color; + if (options.grid.tickColor == null) + options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + // fill in defaults in axes, copy at least always the + // first as the rest of the code assumes it'll be there + for (i = 0; i < Math.max(1, options.xaxes.length); ++i) + options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]); + for (i = 0; i < Math.max(1, options.yaxes.length); ++i) + options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]); + // backwards compatibility, to be removed in future if (options.xaxis.noTicks && options.xaxis.ticks == null) options.xaxis.ticks = options.xaxis.noTicks; if (options.yaxis.noTicks && options.yaxis.ticks == null) options.yaxis.ticks = options.yaxis.noTicks; + if (options.x2axis) { + options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); + options.xaxes[1].position = "top"; + } + if (options.y2axis) { + options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); + options.yaxes[1].position = "right"; + } if (options.grid.coloredAreas) options.grid.markings = options.grid.coloredAreas; if (options.grid.coloredAreasColor) @@ -218,9 +279,16 @@ $.extend(true, options.series.points, options.points); if (options.bars) $.extend(true, options.series.bars, options.bars); - if (options.shadowSize) + if (options.shadowSize != null) options.series.shadowSize = options.shadowSize; + // save options on axes for future reference + for (i = 0; i < options.xaxes.length; ++i) + getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; + for (i = 0; i < options.yaxes.length; ++i) + getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; + + // add hooks from options for (var n in hooks) if (options.hooks[n] && options.hooks[n].length) hooks[n] = hooks[n].concat(options.hooks[n]); @@ -239,7 +307,7 @@ for (var i = 0; i < d.length; ++i) { var s = $.extend(true, {}, options.series); - if (d[i].data) { + if (d[i].data != null) { s.data = d[i].data; // move the data instead of deep-copy delete d[i].data; @@ -255,15 +323,89 @@ return res; } - function axisSpecToRealAxis(obj, attr) { - var a = obj[attr]; - if (!a || a == 1) - return axes[attr]; - if (typeof a == "number") - return axes[attr.charAt(0) + a + attr.slice(1)]; - return a; // assume it's OK + function axisNumber(obj, coord) { + var a = obj[coord + "axis"]; + if (typeof a == "object") // if we got a real axis, extract number + a = a.n; + if (typeof a != "number") + a = 1; // default to first axis + return a; + } + + function allAxes() { + // return flat array without annoying null entries + return $.grep(xaxes.concat(yaxes), function (a) { return a; }); + } + + function canvasToAxisCoords(pos) { + // return an object with x/y corresponding to all used axes + var res = {}, i, axis; + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) + res["x" + axis.n] = axis.c2p(pos.left); + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) + res["y" + axis.n] = axis.c2p(pos.top); + } + + if (res.x1 !== undefined) + res.x = res.x1; + if (res.y1 !== undefined) + res.y = res.y1; + + return res; + } + + function axisToCanvasCoords(pos) { + // get canvas coords from the first pair of x/y found in pos + var res = {}, i, axis, key; + + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) { + key = "x" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "x"; + + if (pos[key] != null) { + res.left = axis.p2c(pos[key]); + break; + } + } + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) { + key = "y" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "y"; + + if (pos[key] != null) { + res.top = axis.p2c(pos[key]); + break; + } + } + } + + return res; } + function getOrCreateAxis(axes, number) { + if (!axes[number - 1]) + axes[number - 1] = { + n: number, // save the number for future reference + direction: axes == xaxes ? "x" : "y", + options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) + }; + + return axes[number - 1]; + } + function fillInSeriesOptions() { var i; @@ -330,7 +472,7 @@ if (s.lines.show == null) { var v, show = true; for (v in s) - if (s[v].show) { + if (s[v] && s[v].show) { show = false; break; } @@ -339,30 +481,32 @@ } // setup axes - s.xaxis = axisSpecToRealAxis(s, "xaxis"); - s.yaxis = axisSpecToRealAxis(s, "yaxis"); + s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); + s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); } } function processData() { var topSentry = Number.POSITIVE_INFINITY, bottomSentry = Number.NEGATIVE_INFINITY, + fakeInfinity = Number.MAX_VALUE, i, j, k, m, length, s, points, ps, x, y, axis, val, f, p; - for (axis in axes) { - axes[axis].datamin = topSentry; - axes[axis].datamax = bottomSentry; - axes[axis].used = false; - } - function updateAxis(axis, min, max) { - if (min < axis.datamin) + if (min < axis.datamin && min != -fakeInfinity) axis.datamin = min; - if (max > axis.datamax) + if (max > axis.datamax && max != fakeInfinity) axis.datamax = max; } + $.each(allAxes(), function (_, axis) { + // init axis + axis.datamin = topSentry; + axis.datamax = bottomSentry; + axis.used = false; + }); + for (i = 0; i < series.length; ++i) { s = series[i]; s.datapoints = { points: [] }; @@ -382,8 +526,13 @@ format.push({ x: true, number: true, required: true }); format.push({ y: true, number: true, required: true }); - if (s.bars.show) + if (s.bars.show || (s.lines.show && s.lines.fill)) { format.push({ y: true, number: true, required: false, defaultValue: 0 }); + if (s.bars.horizontal) { + delete format[format.length - 1].y; + format[format.length - 1].x = true; + } + } s.datapoints.format = format; } @@ -391,8 +540,7 @@ if (s.datapoints.pointsize != null) continue; // already filled in - if (s.datapoints.pointsize == null) - s.datapoints.pointsize = format.length; + s.datapoints.pointsize = format.length; ps = s.datapoints.pointsize; points = s.datapoints.points; @@ -414,6 +562,10 @@ val = +val; // convert to number if (isNaN(val)) val = null; + else if (val == Infinity) + val = fakeInfinity; + else if (val == -Infinity) + val = -fakeInfinity; } if (val == null) { @@ -488,7 +640,7 @@ for (m = 0; m < ps; ++m) { val = points[j + m]; f = format[m]; - if (!f) + if (!f || val == fakeInfinity || val == -fakeInfinity) continue; if (f.x) { @@ -523,54 +675,123 @@ updateAxis(s.yaxis, ymin, ymax); } - for (axis in axes) { - if (axes[axis].datamin == topSentry) - axes[axis].datamin = null; - if (axes[axis].datamax == bottomSentry) - axes[axis].datamax = null; - } + $.each(allAxes(), function (_, axis) { + if (axis.datamin == topSentry) + axis.datamin = null; + if (axis.datamax == bottomSentry) + axis.datamax = null; + }); } - function constructCanvas() { - function makeCanvas(width, height) { - var c = document.createElement('canvas'); - c.width = width; - c.height = height; - if ($.browser.msie) // excanvas hack - c = window.G_vmlCanvasManager.initElement(c); - return c; - } + function makeCanvas(skipPositioning, cls) { + var c = document.createElement('canvas'); + c.className = cls; + c.width = canvasWidth; + c.height = canvasHeight; + + if (!skipPositioning) + $(c).css({ position: 'absolute', left: 0, top: 0 }); + + $(c).appendTo(placeholder); + + if (!c.getContext) // excanvas hack + c = window.G_vmlCanvasManager.initElement(c); + + // used for resetting in case we get replotted + c.getContext("2d").save(); + return c; + } + + function getCanvasDimensions() { canvasWidth = placeholder.width(); canvasHeight = placeholder.height(); - placeholder.html(""); // clear placeholder - if (placeholder.css("position") == 'static') - placeholder.css("position", "relative"); // for positioning labels and overlay - + if (canvasWidth <= 0 || canvasHeight <= 0) throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight; + } + + function resizeCanvas(c) { + // resizing should reset the state (excanvas seems to be + // buggy though) + if (c.width != canvasWidth) + c.width = canvasWidth; + + if (c.height != canvasHeight) + c.height = canvasHeight; - if ($.browser.msie) // excanvas hack - window.G_vmlCanvasManager.init_(document); // make sure everything is setup + // so try to get back to the initial state (even if it's + // gone now, this should be safe according to the spec) + var cctx = c.getContext("2d"); + cctx.restore(); + + // and save again + cctx.save(); + } + + function setupCanvases() { + var reused, + existingCanvas = placeholder.children("canvas.base"), + existingOverlay = placeholder.children("canvas.overlay"); + + if (existingCanvas.length == 0 || existingOverlay == 0) { + // init everything + + placeholder.html(""); // make sure placeholder is clear - // the canvas - canvas = $(makeCanvas(canvasWidth, canvasHeight)).appendTo(placeholder).get(0); - ctx = canvas.getContext("2d"); + placeholder.css({ padding: 0 }); // padding messes up the positioning + + if (placeholder.css("position") == 'static') + placeholder.css("position", "relative"); // for positioning labels and overlay - // overlay canvas for interactive features - overlay = $(makeCanvas(canvasWidth, canvasHeight)).css({ position: 'absolute', left: 0, top: 0 }).appendTo(placeholder).get(0); + getCanvasDimensions(); + + canvas = makeCanvas(true, "base"); + overlay = makeCanvas(false, "overlay"); // overlay canvas for interactive features + + reused = false; + } + else { + // reuse existing elements + + canvas = existingCanvas.get(0); + overlay = existingOverlay.get(0); + + reused = true; + } + + ctx = canvas.getContext("2d"); octx = overlay.getContext("2d"); - octx.stroke(); - } - function bindEvents() { // we include the canvas in the event holder too, because IE 7 // sometimes has trouble with the stacking order eventHolder = $([overlay, canvas]); + if (reused) { + // run shutdown in the old plot object + placeholder.data("plot").shutdown(); + + // reset reused canvases + plot.resize(); + + // make sure overlay pixels are cleared (canvas is cleared when we redraw) + octx.clearRect(0, 0, canvasWidth, canvasHeight); + + // then whack any remaining obvious garbage left + eventHolder.unbind(); + placeholder.children().not([canvas, overlay]).remove(); + } + + // save in case we get replotted + placeholder.data("plot", plot); + } + + function bindEvents() { // bind events - if (options.grid.hoverable) + if (options.grid.hoverable) { eventHolder.mousemove(onMouseMove); + eventHolder.mouseleave(onMouseLeave); + } if (options.grid.clickable) eventHolder.click(onClick); @@ -578,183 +799,293 @@ executeHooks(hooks.bindEvents, [eventHolder]); } - function setupGrid() { - function setTransformationHelpers(axis, o) { - function identity(x) { return x; } - - var s, m, t = o.transform || identity, - it = o.inverseTransform; - - // add transformation helpers - if (axis == axes.xaxis || axis == axes.x2axis) { - // precompute how much the axis is scaling a point - // in canvas space - s = axis.scale = plotWidth / (t(axis.max) - t(axis.min)); - m = t(axis.min); - - // data point to canvas coordinate - if (t == identity) // slight optimization - axis.p2c = function (p) { return (p - m) * s; }; - else - axis.p2c = function (p) { return (t(p) - m) * s; }; - // canvas coordinate to data point - if (!it) - axis.c2p = function (c) { return m + c / s; }; - else - axis.c2p = function (c) { return it(m + c / s); }; - } - else { - s = axis.scale = plotHeight / (t(axis.max) - t(axis.min)); - m = t(axis.max); - - if (t == identity) - axis.p2c = function (p) { return (m - p) * s; }; - else - axis.p2c = function (p) { return (m - t(p)) * s; }; - if (!it) - axis.c2p = function (c) { return m - c / s; }; - else - axis.c2p = function (c) { return it(m - c / s); }; - } + function shutdown() { + if (redrawTimeout) + clearTimeout(redrawTimeout); + + eventHolder.unbind("mousemove", onMouseMove); + eventHolder.unbind("mouseleave", onMouseLeave); + eventHolder.unbind("click", onClick); + + executeHooks(hooks.shutdown, [eventHolder]); + } + + function setTransformationHelpers(axis) { + // set helper functions on the axis, assumes plot area + // has been computed already + + function identity(x) { return x; } + + var s, m, t = axis.options.transform || identity, + it = axis.options.inverseTransform; + + // precompute how much the axis is scaling a point + // in canvas space + if (axis.direction == "x") { + s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); + m = Math.min(t(axis.max), t(axis.min)); + } + else { + s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); + s = -s; + m = Math.max(t(axis.max), t(axis.min)); } - function measureLabels(axis, axisOptions) { - var i, labels = [], l; - - axis.labelWidth = axisOptions.labelWidth; - axis.labelHeight = axisOptions.labelHeight; - - if (axis == axes.xaxis || axis == axes.x2axis) { - // to avoid measuring the widths of the labels, we - // construct fixed-size boxes and put the labels inside - // them, we don't need the exact figures and the - // fixed-size box content is easy to center - if (axis.labelWidth == null) - axis.labelWidth = canvasWidth / (axis.ticks.length > 0 ? axis.ticks.length : 1); - - // measure x label heights - if (axis.labelHeight == null) { - labels = []; - for (i = 0; i < axis.ticks.length; ++i) { - l = axis.ticks[i].label; - if (l) - labels.push('
' + l + '
'); - } - - if (labels.length > 0) { - var dummyDiv = $('
' - + labels.join("") + '
').appendTo(placeholder); - axis.labelHeight = dummyDiv.height(); - dummyDiv.remove(); - } - } - } - else if (axis.labelWidth == null || axis.labelHeight == null) { - // calculate y label dimensions - for (i = 0; i < axis.ticks.length; ++i) { - l = axis.ticks[i].label; + // data point to canvas coordinate + if (t == identity) // slight optimization + axis.p2c = function (p) { return (p - m) * s; }; + else + axis.p2c = function (p) { return (t(p) - m) * s; }; + // canvas coordinate to data point + if (!it) + axis.c2p = function (c) { return m + c / s; }; + else + axis.c2p = function (c) { return it(m + c / s); }; + } + + function measureTickLabels(axis) { + var opts = axis.options, i, ticks = axis.ticks || [], labels = [], + l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv; + + function makeDummyDiv(labels, width) { + return $('
' + + '
' + + labels.join("") + '
') + .appendTo(placeholder); + } + + if (axis.direction == "x") { + // to avoid measuring the widths of the labels (it's slow), we + // construct fixed-size boxes and put the labels inside + // them, we don't need the exact figures and the + // fixed-size box content is easy to center + if (w == null) + w = Math.floor(canvasWidth / (ticks.length > 0 ? ticks.length : 1)); + + // measure x label heights + if (h == null) { + labels = []; + for (i = 0; i < ticks.length; ++i) { + l = ticks[i].label; if (l) - labels.push('
' + l + '
'); + labels.push('
' + l + '
'); } - + if (labels.length > 0) { - var dummyDiv = $('
' - + labels.join("") + '
').appendTo(placeholder); - if (axis.labelWidth == null) - axis.labelWidth = dummyDiv.width(); - if (axis.labelHeight == null) - axis.labelHeight = dummyDiv.find("div").height(); + // stick them all in the same div and measure + // collective height + labels.push('
'); + dummyDiv = makeDummyDiv(labels, "width:10000px;"); + h = dummyDiv.height(); dummyDiv.remove(); } - } - - if (axis.labelWidth == null) - axis.labelWidth = 0; - if (axis.labelHeight == null) - axis.labelHeight = 0; } - - function setGridSpacing() { - // get the most space needed around the grid for things - // that may stick out - var maxOutset = options.grid.borderWidth; - for (i = 0; i < series.length; ++i) - maxOutset = Math.max(maxOutset, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); + else if (w == null || h == null) { + // calculate y label dimensions + for (i = 0; i < ticks.length; ++i) { + l = ticks[i].label; + if (l) + labels.push('
' + l + '
'); + } - plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = maxOutset; + if (labels.length > 0) { + dummyDiv = makeDummyDiv(labels, ""); + if (w == null) + w = dummyDiv.children().width(); + if (h == null) + h = dummyDiv.find("div.tickLabel").height(); + dummyDiv.remove(); + } + } + + if (w == null) + w = 0; + if (h == null) + h = 0; + + axis.labelWidth = w; + axis.labelHeight = h; + } + + function allocateAxisBoxFirstPhase(axis) { + // find the bounding box of the axis by looking at label + // widths/heights and ticks, make room by diminishing the + // plotOffset + + var lw = axis.labelWidth, + lh = axis.labelHeight, + pos = axis.options.position, + tickLength = axis.options.tickLength, + axismargin = options.grid.axisMargin, + padding = options.grid.labelMargin, + all = axis.direction == "x" ? xaxes : yaxes, + index; + + // determine axis margin + var samePosition = $.grep(all, function (a) { + return a && a.options.position == pos && a.reserveSpace; + }); + if ($.inArray(axis, samePosition) == samePosition.length - 1) + axismargin = 0; // outermost + + // determine tick length - if we're innermost, we can use "full" + if (tickLength == null) + tickLength = "full"; + + var sameDirection = $.grep(all, function (a) { + return a && a.reserveSpace; + }); + + var innermost = $.inArray(axis, sameDirection) == 0; + if (!innermost && tickLength == "full") + tickLength = 5; - var margin = options.grid.labelMargin + options.grid.borderWidth; + if (!isNaN(+tickLength)) + padding += +tickLength; + + // compute box + if (axis.direction == "x") { + lh += padding; - if (axes.xaxis.labelHeight > 0) - plotOffset.bottom = Math.max(maxOutset, axes.xaxis.labelHeight + margin); - if (axes.yaxis.labelWidth > 0) - plotOffset.left = Math.max(maxOutset, axes.yaxis.labelWidth + margin); - if (axes.x2axis.labelHeight > 0) - plotOffset.top = Math.max(maxOutset, axes.x2axis.labelHeight + margin); - if (axes.y2axis.labelWidth > 0) - plotOffset.right = Math.max(maxOutset, axes.y2axis.labelWidth + margin); - - plotWidth = canvasWidth - plotOffset.left - plotOffset.right; - plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top; + if (pos == "bottom") { + plotOffset.bottom += lh + axismargin; + axis.box = { top: canvasHeight - plotOffset.bottom, height: lh }; + } + else { + axis.box = { top: plotOffset.top + axismargin, height: lh }; + plotOffset.top += lh + axismargin; + } } - - var axis; - for (axis in axes) - setRange(axes[axis], options[axis]); - - if (options.grid.show) { - for (axis in axes) { - prepareTickGeneration(axes[axis], options[axis]); - setTicks(axes[axis], options[axis]); - measureLabels(axes[axis], options[axis]); + else { + lw += padding; + + if (pos == "left") { + axis.box = { left: plotOffset.left + axismargin, width: lw }; + plotOffset.left += lw + axismargin; + } + else { + plotOffset.right += lw + axismargin; + axis.box = { left: canvasWidth - plotOffset.right, width: lw }; } + } + + // save for future reference + axis.position = pos; + axis.tickLength = tickLength; + axis.box.padding = padding; + axis.innermost = innermost; + } - setGridSpacing(); + function allocateAxisBoxSecondPhase(axis) { + // set remaining bounding box coordinates + if (axis.direction == "x") { + axis.box.left = plotOffset.left; + axis.box.width = plotWidth; } else { - plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0; - plotWidth = canvasWidth; - plotHeight = canvasHeight; + axis.box.top = plotOffset.top; + axis.box.height = plotHeight; + } + } + + function setupGrid() { + var i, axes = allAxes(); + + // first calculate the plot and axis box dimensions + + $.each(axes, function (_, axis) { + axis.show = axis.options.show; + if (axis.show == null) + axis.show = axis.used; // by default an axis is visible if it's got data + + axis.reserveSpace = axis.show || axis.options.reserveSpace; + + setRange(axis); + }); + + allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; }); + + plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0; + if (options.grid.show) { + $.each(allocatedAxes, function (_, axis) { + // make the ticks + setupTickGeneration(axis); + setTicks(axis); + snapRangeToTicks(axis, axis.ticks); + + // find labelWidth/Height for axis + measureTickLabels(axis); + }); + + // with all dimensions in house, we can compute the + // axis boxes, start from the outside (reverse order) + for (i = allocatedAxes.length - 1; i >= 0; --i) + allocateAxisBoxFirstPhase(allocatedAxes[i]); + + // make sure we've got enough space for things that + // might stick out + var minMargin = options.grid.minBorderMargin; + if (minMargin == null) { + minMargin = 0; + for (i = 0; i < series.length; ++i) + minMargin = Math.max(minMargin, series[i].points.radius + series[i].points.lineWidth/2); + } + + for (var a in plotOffset) { + plotOffset[a] += options.grid.borderWidth; + plotOffset[a] = Math.max(minMargin, plotOffset[a]); + } } - for (axis in axes) - setTransformationHelpers(axes[axis], options[axis]); + plotWidth = canvasWidth - plotOffset.left - plotOffset.right; + plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top; - if (options.grid.show) - insertLabels(); + // now we got the proper plotWidth/Height, we can compute the scaling + $.each(axes, function (_, axis) { + setTransformationHelpers(axis); + }); + + if (options.grid.show) { + $.each(allocatedAxes, function (_, axis) { + allocateAxisBoxSecondPhase(axis); + }); + + insertAxisLabels(); + } insertLegend(); } - function setRange(axis, axisOptions) { - var min = +(axisOptions.min != null ? axisOptions.min : axis.datamin), - max = +(axisOptions.max != null ? axisOptions.max : axis.datamax), + function setRange(axis) { + var opts = axis.options, + min = +(opts.min != null ? opts.min : axis.datamin), + max = +(opts.max != null ? opts.max : axis.datamax), delta = max - min; if (delta == 0.0) { // degenerate case var widen = max == 0 ? 1 : 0.01; - if (axisOptions.min == null) + if (opts.min == null) min -= widen; - // alway widen max if we couldn't widen min to ensure we + // always widen max if we couldn't widen min to ensure we // don't fall into min == max which doesn't work - if (axisOptions.max == null || axisOptions.min != null) + if (opts.max == null || opts.min != null) max += widen; } else { // consider autoscaling - var margin = axisOptions.autoscaleMargin; + var margin = opts.autoscaleMargin; if (margin != null) { - if (axisOptions.min == null) { + if (opts.min == null) { min -= delta * margin; // make sure we don't go below zero if all values // are positive if (min < 0 && axis.datamin != null && axis.datamin >= 0) min = 0; } - if (axisOptions.max == null) { + if (opts.max == null) { max += delta * margin; if (max > 0 && axis.datamax != null && axis.datamax <= 0) max = 0; @@ -765,22 +1096,22 @@ axis.max = max; } - function prepareTickGeneration(axis, axisOptions) { + function setupTickGeneration(axis) { + var opts = axis.options; + // estimate number of ticks var noTicks; - if (typeof axisOptions.ticks == "number" && axisOptions.ticks > 0) - noTicks = axisOptions.ticks; - else if (axis == axes.xaxis || axis == axes.x2axis) - // heuristic based on the model a*sqrt(x) fitted to - // some reasonable data points - noTicks = 0.3 * Math.sqrt(canvasWidth); + if (typeof opts.ticks == "number" && opts.ticks > 0) + noTicks = opts.ticks; else - noTicks = 0.3 * Math.sqrt(canvasHeight); - + // heuristic based on the model a*sqrt(x) fitted to + // some data points that seemed reasonable + noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight); + var delta = (axis.max - axis.min) / noTicks, size, generator, unit, formatter, i, magn, norm; - if (axisOptions.mode == "time") { + if (opts.mode == "time") { // pretty handling of time // map of app. size of time units in milliseconds @@ -810,14 +1141,14 @@ ]; var minSize = 0; - if (axisOptions.minTickSize != null) { - if (typeof axisOptions.tickSize == "number") - minSize = axisOptions.tickSize; + if (opts.minTickSize != null) { + if (typeof opts.tickSize == "number") + minSize = opts.tickSize; else - minSize = axisOptions.minTickSize[0] * timeUnitSize[axisOptions.minTickSize[1]]; + minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; } - for (i = 0; i < spec.length - 1; ++i) + for (var i = 0; i < spec.length - 1; ++i) if (delta < (spec[i][0] * timeUnitSize[spec[i][1]] + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) @@ -841,10 +1172,7 @@ size *= magn; } - if (axisOptions.tickSize) { - size = axisOptions.tickSize[0]; - unit = axisOptions.tickSize[1]; - } + axis.tickSize = opts.tickSize || [size, unit]; generator = function(axis) { var ticks = [], @@ -882,7 +1210,7 @@ do { prev = v; v = d.getTime(); - ticks.push({ v: v, label: axis.tickFormatter(v, axis) }); + ticks.push(v); if (unit == "month") { if (tickSize < 1) { // a bit complicated - we'll divide the month @@ -913,12 +1241,12 @@ var d = new Date(v); // first check global format - if (axisOptions.timeformat != null) - return $.plot.formatDate(d, axisOptions.timeformat, axisOptions.monthNames); + if (opts.timeformat != null) + return $.plot.formatDate(d, opts.timeformat, opts.monthNames); var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; var span = axis.max - axis.min; - var suffix = (axisOptions.twelveHourClock) ? " %p" : ""; + var suffix = (opts.twelveHourClock) ? " %p" : ""; if (t < timeUnitSize.minute) fmt = "%h:%M:%S" + suffix; @@ -939,12 +1267,12 @@ else fmt = "%y"; - return $.plot.formatDate(d, fmt, axisOptions.monthNames); + return $.plot.formatDate(d, fmt, opts.monthNames); }; } else { // pretty rounding of base-10 numbers - var maxDec = axisOptions.tickDecimals; + var maxDec = opts.tickDecimals; var dec = -Math.floor(Math.log(delta) / Math.LN10); if (maxDec != null && dec > maxDec) dec = maxDec; @@ -969,13 +1297,11 @@ size *= magn; - if (axisOptions.minTickSize != null && size < axisOptions.minTickSize) - size = axisOptions.minTickSize; - - if (axisOptions.tickSize != null) - size = axisOptions.tickSize; + if (opts.minTickSize != null && size < opts.minTickSize) + size = opts.minTickSize; - axis.tickDecimals = Math.max(0, (maxDec != null) ? maxDec : dec); + axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); + axis.tickSize = opts.tickSize || size; generator = function (axis) { var ticks = []; @@ -986,7 +1312,7 @@ do { prev = v; v = start + i * axis.tickSize; - ticks.push({ v: v, label: axis.tickFormatter(v, axis) }); + ticks.push(v); ++i; } while (v < axis.max && v != prev); return ticks; @@ -997,57 +1323,90 @@ }; } - axis.tickSize = unit ? [size, unit] : size; + if (opts.alignTicksWithAxis != null) { + var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; + if (otherAxis && otherAxis.used && otherAxis != axis) { + // consider snapping min/max to outermost nice ticks + var niceTicks = generator(axis); + if (niceTicks.length > 0) { + if (opts.min == null) + axis.min = Math.min(axis.min, niceTicks[0]); + if (opts.max == null && niceTicks.length > 1) + axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); + } + + generator = function (axis) { + // copy ticks, scaled to this axis + var ticks = [], v, i; + for (i = 0; i < otherAxis.ticks.length; ++i) { + v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); + v = axis.min + v * (axis.max - axis.min); + ticks.push(v); + } + return ticks; + }; + + // we might need an extra decimal since forced + // ticks don't necessarily fit naturally + if (axis.mode != "time" && opts.tickDecimals == null) { + var extraDec = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1), + ts = generator(axis); + + // only proceed if the tick interval rounded + // with an extra decimal doesn't give us a + // zero at end + if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) + axis.tickDecimals = extraDec; + } + } + } + axis.tickGenerator = generator; - if ($.isFunction(axisOptions.tickFormatter)) - axis.tickFormatter = function (v, axis) { return "" + axisOptions.tickFormatter(v, axis); }; + if ($.isFunction(opts.tickFormatter)) + axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; else axis.tickFormatter = formatter; } - function setTicks(axis, axisOptions) { - axis.ticks = []; - - if (!axis.used) - return; - - if (axisOptions.ticks == null) - axis.ticks = axis.tickGenerator(axis); - else if (typeof axisOptions.ticks == "number") { - if (axisOptions.ticks > 0) - axis.ticks = axis.tickGenerator(axis); + function setTicks(axis) { + var oticks = axis.options.ticks, ticks = []; + if (oticks == null || (typeof oticks == "number" && oticks > 0)) + ticks = axis.tickGenerator(axis); + else if (oticks) { + if ($.isFunction(oticks)) + // generate the ticks + ticks = oticks({ min: axis.min, max: axis.max }); + else + ticks = oticks; } - else if (axisOptions.ticks) { - var ticks = axisOptions.ticks; - if ($.isFunction(ticks)) - // generate the ticks - ticks = ticks({ min: axis.min, max: axis.max }); - - // clean up the user-supplied ticks, copy them over - var i, v; - for (i = 0; i < ticks.length; ++i) { - var label = null; - var t = ticks[i]; - if (typeof t == "object") { - v = t[0]; - if (t.length > 1) - label = t[1]; - } - else - v = t; - if (label == null) - label = axis.tickFormatter(v, axis); - axis.ticks[i] = { v: v, label: label }; + // clean up/labelify the supplied ticks, copy them over + var i, v; + axis.ticks = []; + for (i = 0; i < ticks.length; ++i) { + var label = null; + var t = ticks[i]; + if (typeof t == "object") { + v = +t[0]; + if (t.length > 1) + label = t[1]; } + else + v = +t; + if (label == null) + label = axis.tickFormatter(v, axis); + if (!isNaN(v)) + axis.ticks.push({ v: v, label: label }); } + } - if (axisOptions.autoscaleMargin != null && axis.ticks.length > 0) { + function snapRangeToTicks(axis, ticks) { + if (axis.options.autoscaleMargin && ticks.length > 0) { // snap to ticks - if (axisOptions.min == null) - axis.min = Math.min(axis.min, axis.ticks[0].v); - if (axisOptions.max == null && axis.ticks.length > 1) - axis.max = Math.max(axis.max, axis.ticks[axis.ticks.length - 1].v); + if (axis.options.min == null) + axis.min = Math.min(axis.min, ticks[0].v); + if (axis.options.max == null && ticks.length > 1) + axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); } } @@ -1055,12 +1414,18 @@ ctx.clearRect(0, 0, canvasWidth, canvasHeight); var grid = options.grid; + + // draw background, if any + if (grid.show && grid.backgroundColor) + drawBackground(); if (grid.show && !grid.aboveData) drawGrid(); - for (var i = 0; i < series.length; ++i) + for (var i = 0; i < series.length; ++i) { + executeHooks(hooks.drawSeries, [ctx, series[i]]); drawSeries(series[i]); + } executeHooks(hooks.draw, [ctx]); @@ -1069,52 +1434,68 @@ } function extractRange(ranges, coord) { - var firstAxis = coord + "axis", - secondaryAxis = coord + "2axis", - axis, from, to, reverse; - - if (ranges[firstAxis]) { - axis = axes[firstAxis]; - from = ranges[firstAxis].from; - to = ranges[firstAxis].to; - } - else if (ranges[secondaryAxis]) { - axis = axes[secondaryAxis]; - from = ranges[secondaryAxis].from; - to = ranges[secondaryAxis].to; + var axis, from, to, key, axes = allAxes(); + + for (i = 0; i < axes.length; ++i) { + axis = axes[i]; + if (axis.direction == coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n == 1) + key = coord + "axis"; // support x1axis as xaxis + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } } - else { - // backwards-compat stuff - to be removed in future - axis = axes[firstAxis]; + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord == "x" ? xaxes[0] : yaxes[0]; from = ranges[coord + "1"]; to = ranges[coord + "2"]; } // auto-reverse as an added bonus - if (from != null && to != null && from > to) - return { from: to, to: from, axis: axis }; + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } return { from: from, to: to, axis: axis }; } + function drawBackground() { + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); + ctx.fillRect(0, 0, plotWidth, plotHeight); + ctx.restore(); + } + function drawGrid() { var i; ctx.save(); ctx.translate(plotOffset.left, plotOffset.top); - // draw background, if any - if (options.grid.backgroundColor) { - ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); - ctx.fillRect(0, 0, plotWidth, plotHeight); - } - // draw markings var markings = options.grid.markings; if (markings) { - if ($.isFunction(markings)) - // xmin etc. are backwards-compatible, to be removed in future - markings = markings({ xmin: axes.xaxis.min, xmax: axes.xaxis.max, ymin: axes.yaxis.min, ymax: axes.yaxis.max, xaxis: axes.xaxis, yaxis: axes.yaxis, x2axis: axes.x2axis, y2axis: axes.y2axis }); + if ($.isFunction(markings)) { + var axes = plot.getAxes(); + // xmin etc. is backwards compatibility, to be + // removed in the future + axes.xmin = axes.xaxis.min; + axes.xmax = axes.xaxis.max; + axes.ymin = axes.yaxis.min; + axes.ymax = axes.yaxis.max; + + markings = markings(axes); + } for (i = 0; i < markings.length; ++i) { var m = markings[i], @@ -1155,8 +1536,6 @@ ctx.beginPath(); ctx.strokeStyle = m.color || options.grid.markingsColor; ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth; - //ctx.moveTo(Math.floor(xrange.from), yrange.from); - //ctx.lineTo(Math.floor(xrange.to), yrange.to); ctx.moveTo(xrange.from, yrange.from); ctx.lineTo(xrange.to, yrange.to); ctx.stroke(); @@ -1171,55 +1550,98 @@ } } - // draw the inner grid - ctx.lineWidth = 1; - ctx.strokeStyle = options.grid.tickColor; - ctx.beginPath(); - var v, axis = axes.xaxis; - for (i = 0; i < axis.ticks.length; ++i) { - v = axis.ticks[i].v; - if (v <= axis.min || v >= axes.xaxis.max) - continue; // skip those lying on the axes - - ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 0); - ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, plotHeight); - } - - axis = axes.yaxis; - for (i = 0; i < axis.ticks.length; ++i) { - v = axis.ticks[i].v; - if (v <= axis.min || v >= axis.max) - continue; + // draw the ticks + var axes = allAxes(), bw = options.grid.borderWidth; + + for (var j = 0; j < axes.length; ++j) { + var axis = axes[j], box = axis.box, + t = axis.tickLength, x, y, xoff, yoff; + if (!axis.show || axis.ticks.length == 0) + continue + + ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString(); + ctx.lineWidth = 1; + + // find the edges + if (axis.direction == "x") { + x = 0; + if (t == "full") + y = (axis.position == "top" ? 0 : plotHeight); + else + y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); + } + else { + y = 0; + if (t == "full") + x = (axis.position == "left" ? 0 : plotWidth); + else + x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); + } + + // draw tick bar + if (!axis.innermost) { + ctx.beginPath(); + xoff = yoff = 0; + if (axis.direction == "x") + xoff = plotWidth; + else + yoff = plotHeight; + + if (ctx.lineWidth == 1) { + x = Math.floor(x) + 0.5; + y = Math.floor(y) + 0.5; + } - ctx.moveTo(0, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); - ctx.lineTo(plotWidth, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); - } + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + ctx.stroke(); + } - axis = axes.x2axis; - for (i = 0; i < axis.ticks.length; ++i) { - v = axis.ticks[i].v; - if (v <= axis.min || v >= axis.max) - continue; - - ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, -5); - ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 5); - } + // draw ticks + ctx.beginPath(); + for (i = 0; i < axis.ticks.length; ++i) { + var v = axis.ticks[i].v; + + xoff = yoff = 0; - axis = axes.y2axis; - for (i = 0; i < axis.ticks.length; ++i) { - v = axis.ticks[i].v; - if (v <= axis.min || v >= axis.max) - continue; + if (v < axis.min || v > axis.max + // skip those lying on the axes if we got a border + || (t == "full" && bw > 0 + && (v == axis.min || v == axis.max))) + continue; - ctx.moveTo(plotWidth-5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); - ctx.lineTo(plotWidth+5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); + if (axis.direction == "x") { + x = axis.p2c(v); + yoff = t == "full" ? -plotHeight : t; + + if (axis.position == "top") + yoff = -yoff; + } + else { + y = axis.p2c(v); + xoff = t == "full" ? -plotWidth : t; + + if (axis.position == "left") + xoff = -xoff; + } + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") + x = Math.floor(x) + 0.5; + else + y = Math.floor(y) + 0.5; + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + } + + ctx.stroke(); } - ctx.stroke(); - if (options.grid.borderWidth) { - // draw border - var bw = options.grid.borderWidth; + // draw border + if (bw) { ctx.lineWidth = bw; ctx.strokeStyle = options.grid.borderColor; ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); @@ -1228,41 +1650,58 @@ ctx.restore(); } - function insertLabels() { + function insertAxisLabels() { placeholder.find(".tickLabels").remove(); - var html = ['
']; + var html = ['
']; - function addLabels(axis, labelGenerator) { + var axes = allAxes(); + for (var j = 0; j < axes.length; ++j) { + var axis = axes[j], box = axis.box; + if (!axis.show) + continue; + //debug: html.push('
') + html.push('
'); for (var i = 0; i < axis.ticks.length; ++i) { var tick = axis.ticks[i]; if (!tick.label || tick.v < axis.min || tick.v > axis.max) continue; - html.push(labelGenerator(tick, axis)); + + var pos = {}, align; + + if (axis.direction == "x") { + align = "center"; + pos.left = Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2); + if (axis.position == "bottom") + pos.top = box.top + box.padding; + else + pos.bottom = canvasHeight - (box.top + box.height - box.padding); + } + else { + pos.top = Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2); + if (axis.position == "left") { + pos.right = canvasWidth - (box.left + box.width - box.padding) + align = "right"; + } + else { + pos.left = box.left + box.padding; + align = "left"; + } + } + + pos.width = axis.labelWidth; + + var style = ["position:absolute", "text-align:" + align ]; + for (var a in pos) + style.push(a + ":" + pos[a] + "px") + + html.push('
' + tick.label + '
'); } + html.push('
'); } - var margin = options.grid.labelMargin + options.grid.borderWidth; - - addLabels(axes.xaxis, function (tick, axis) { - return '
' + tick.label + "
"; - }); - - - addLabels(axes.yaxis, function (tick, axis) { - return '
' + tick.label + "
"; - }); - - addLabels(axes.x2axis, function (tick, axis) { - return '
' + tick.label + "
"; - }); - - addLabels(axes.y2axis, function (tick, axis) { - return '
' + tick.label + "
"; - }); - html.push('
'); - + placeholder.append(html.join("")); } @@ -1360,18 +1799,40 @@ var points = datapoints.points, ps = datapoints.pointsize, bottom = Math.min(Math.max(0, axisy.min), axisy.max), - top, lastX = 0, areaOpen = false; - - for (var i = ps; i < points.length; i += ps) { - var x1 = points[i - ps], y1 = points[i - ps + 1], - x2 = points[i], y2 = points[i + 1]; - - if (areaOpen && x1 != null && x2 == null) { - // close area - ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom)); - ctx.fill(); - areaOpen = false; - continue; + i = 0, top, areaOpen = false, + ypos = 1, segmentStart = 0, segmentEnd = 0; + + // we process each segment in two turns, first forward + // direction to sketch out top, then once we hit the + // end we go backwards to sketch the bottom + while (true) { + if (ps > 0 && i > points.length + ps) + break; + + i += ps; // ps is negative if going backwards + + var x1 = points[i - ps], + y1 = points[i - ps + ypos], + x2 = points[i], y2 = points[i + ypos]; + + if (areaOpen) { + if (ps > 0 && x1 != null && x2 == null) { + // at turning point + segmentEnd = i; + ps = -ps; + ypos = 2; + continue; + } + + if (ps < 0 && i == segmentStart + ps) { + // done with the reverse sweep + ctx.fill(); + areaOpen = false; + ps = -ps; + ypos = 1; + i = segmentStart = segmentEnd + ps; + continue; + } } if (x1 == null || x2 == null) @@ -1418,22 +1879,22 @@ if (y1 >= axisy.max && y2 >= axisy.max) { ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); - lastX = x2; continue; } else if (y1 <= axisy.min && y2 <= axisy.min) { ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); - lastX = x2; continue; } // else it's a bit more complicated, there might - // be two rectangles and two triangles we need to fill - // in; to find these keep track of the current x values + // be a flat maxed out rectangle first, then a + // triangular cutout or reverse; to find these + // keep track of the current x values var x1old = x1, x2old = x2; - // and clip the y values, without shortcutting + // clip the y values, without shortcutting, we + // go through all cases in turn // clip with ymin if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { @@ -1455,43 +1916,27 @@ y2 = axisy.max; } - // if the x value was changed we got a rectangle // to fill if (x1 != x1old) { - if (y1 <= axisy.min) - top = axisy.min; - else - top = axisy.max; - - ctx.lineTo(axisx.p2c(x1old), axisy.p2c(top)); - ctx.lineTo(axisx.p2c(x1), axisy.p2c(top)); + ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); + // it goes to (x1, y1), but we fill that below } - // fill the triangles + // fill triangular section, this sometimes result + // in redundant points if (x1, y1) hasn't changed + // from previous line to, but we just ignore that ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); // fill the other rectangle if it's there if (x2 != x2old) { - if (y2 <= axisy.min) - top = axisy.min; - else - top = axisy.max; - - ctx.lineTo(axisx.p2c(x2), axisy.p2c(top)); - ctx.lineTo(axisx.p2c(x2old), axisy.p2c(top)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); } - - lastX = Math.max(x2, x2old); - } - - if (areaOpen) { - ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom)); - ctx.fill(); } } - + ctx.save(); ctx.translate(plotOffset.left, plotOffset.top); ctx.lineJoin = "round"; @@ -1524,16 +1969,23 @@ } function drawSeriesPoints(series) { - function plotPoints(datapoints, radius, fillStyle, offset, circumference, axisx, axisy) { + function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { var points = datapoints.points, ps = datapoints.pointsize; - + for (var i = 0; i < points.length; i += ps) { var x = points[i], y = points[i + 1]; if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) continue; ctx.beginPath(); - ctx.arc(axisx.p2c(x), axisy.p2c(y) + offset, radius, 0, circumference, false); + x = axisx.p2c(x); + y = axisy.p2c(y) + offset; + if (symbol == "circle") + ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); + else + symbol(ctx, x, y, radius, shadow); + ctx.closePath(); + if (fillStyle) { ctx.fillStyle = fillStyle; ctx.fill(); @@ -1545,35 +1997,39 @@ ctx.save(); ctx.translate(plotOffset.left, plotOffset.top); - var lw = series.lines.lineWidth, + var lw = series.points.lineWidth, sw = series.shadowSize, - radius = series.points.radius; + radius = series.points.radius, + symbol = series.points.symbol; if (lw > 0 && sw > 0) { // draw shadow in two steps var w = sw / 2; ctx.lineWidth = w; ctx.strokeStyle = "rgba(0,0,0,0.1)"; - plotPoints(series.datapoints, radius, null, w + w/2, Math.PI, - series.xaxis, series.yaxis); + plotPoints(series.datapoints, radius, null, w + w/2, true, + series.xaxis, series.yaxis, symbol); ctx.strokeStyle = "rgba(0,0,0,0.2)"; - plotPoints(series.datapoints, radius, null, w/2, Math.PI, - series.xaxis, series.yaxis); + plotPoints(series.datapoints, radius, null, w/2, true, + series.xaxis, series.yaxis, symbol); } ctx.lineWidth = lw; ctx.strokeStyle = series.color; plotPoints(series.datapoints, radius, - getFillStyle(series.points, series.color), 0, 2 * Math.PI, - series.xaxis, series.yaxis); + getFillStyle(series.points, series.color), 0, false, + series.xaxis, series.yaxis, symbol); ctx.restore(); } - function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal) { + function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { var left, right, bottom, top, drawLeft, drawRight, drawTop, drawBottom, tmp; + // in horizontal mode, we start the bar from the left + // instead of from the bottom so it appears to be + // horizontal rather than vertical if (horizontal) { drawBottom = drawRight = drawTop = true; drawLeft = false; @@ -1651,7 +2107,7 @@ } // draw outline - if (drawLeft || drawRight || drawTop || drawBottom) { + if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { c.beginPath(); // FIXME: inline moveTo is buggy with excanvas @@ -1683,7 +2139,7 @@ for (var i = 0; i < points.length; i += ps) { if (points[i] == null) continue; - drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal); + drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); } } @@ -1721,7 +2177,7 @@ var fragments = [], rowStarted = false, lf = options.legend.labelFormatter, s, label; - for (i = 0; i < series.length; ++i) { + for (var i = 0; i < series.length; ++i) { s = series[i]; label = s.label; if (!label) @@ -1797,7 +2253,7 @@ smallestDistance = maxDistance * maxDistance + 1, item = null, foundPoint = false, i, j; - for (i = 0; i < series.length; ++i) { + for (i = series.length - 1; i >= 0; --i) { if (!seriesFilter(series[i])) continue; @@ -1811,6 +2267,13 @@ maxx = maxDistance / axisx.scale, maxy = maxDistance / axisy.scale; + // with inverse transforms, we can't use the maxx/maxy + // optimization, sadly + if (axisx.options.inverseTransform) + maxx = Number.MAX_VALUE; + if (axisy.options.inverseTransform) + maxy = Number.MAX_VALUE; + if (s.lines.show || s.points.show) { for (j = 0; j < points.length; j += ps) { var x = points[j], y = points[j + 1]; @@ -1831,7 +2294,7 @@ // use <= to ensure last point takes precedence // (last generally means on top of) - if (dist <= smallestDistance) { + if (dist < smallestDistance) { smallestDistance = dist; item = [i, j / ps]; } @@ -1877,7 +2340,13 @@ triggerClickHoverEvent("plothover", e, function (s) { return s["hoverable"] != false; }); } - + + function onMouseLeave(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return false; }); + } + function onClick(e) { triggerClickHoverEvent("plotclick", e, function (s) { return s["clickable"] != false; }); @@ -1887,18 +2356,12 @@ // so we share their code) function triggerClickHoverEvent(eventname, event, seriesFilter) { var offset = eventHolder.offset(), - pos = { pageX: event.pageX, pageY: event.pageY }, canvasX = event.pageX - offset.left - plotOffset.left, - canvasY = event.pageY - offset.top - plotOffset.top; + canvasY = event.pageY - offset.top - plotOffset.top, + pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); - if (axes.xaxis.used) - pos.x = axes.xaxis.c2p(canvasX); - if (axes.yaxis.used) - pos.y = axes.yaxis.c2p(canvasY); - if (axes.x2axis.used) - pos.x2 = axes.x2axis.c2p(canvasX); - if (axes.y2axis.used) - pos.y2 = axes.y2axis.c2p(canvasY); + pos.pageX = event.pageX; + pos.pageY = event.pageY; var item = findNearbyItem(canvasX, canvasY, seriesFilter); @@ -1913,7 +2376,9 @@ for (var i = 0; i < highlights.length; ++i) { var h = highlights[i]; if (h.auto == eventname && - !(item && h.series == item.series && h.point == item.datapoint)) + !(item && h.series == item.series && + h.point[0] == item.datapoint[0] && + h.point[1] == item.datapoint[1])) unhighlight(h.series, h.point); } @@ -1955,8 +2420,10 @@ if (typeof s == "number") s = series[s]; - if (typeof point == "number") - point = s.data[point]; + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } var i = indexOfHighlight(s, point); if (i == -1) { @@ -2008,9 +2475,16 @@ var pointRadius = series.points.radius + series.points.lineWidth / 2; octx.lineWidth = pointRadius; octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString(); - var radius = 1.5 * pointRadius; + var radius = 1.5 * pointRadius, + x = axisx.p2c(x), + y = axisy.p2c(y); + octx.beginPath(); - octx.arc(axisx.p2c(x), axisy.p2c(y), radius, 0, 2 * Math.PI, false); + if (series.points.symbol == "circle") + octx.arc(x, y, radius, 0, 2 * Math.PI, false); + else + series.points.symbol(octx, x, y, radius, false); + octx.closePath(); octx.stroke(); } @@ -2020,7 +2494,7 @@ var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString(); var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2; drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, - 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal); + 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); } function getColorOrGradient(spec, bottom, top, defaultColor) { @@ -2035,9 +2509,12 @@ for (var i = 0, l = spec.colors.length; i < l; ++i) { var c = spec.colors[i]; if (typeof c != "string") { - c = $.color.parse(defaultColor).scale('rgb', c.brightness); - c.a *= c.opacity; - c = c.toString(); + var co = $.color.parse(defaultColor); + if (c.brightness != null) + co = co.scale('rgb', c.brightness) + if (c.opacity != null) + co.a *= c.opacity; + c = co.toString(); } gradient.addColorStop(i / (l - 1), c); } @@ -2048,17 +2525,14 @@ } $.plot = function(placeholder, data, options) { + //var t0 = new Date(); var plot = new Plot($(placeholder), data, options, $.plot.plugins); - /*var t0 = new Date(); - var t1 = new Date(); - var tstr = "time used (msecs): " + (t1.getTime() - t0.getTime()) - if (window.console) - console.log(tstr); - else - alert(tstr);*/ + //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); return plot; }; + $.plot.version = "0.7"; + $.plot.plugins = []; // returns a string with the date d formatted according to fmt @@ -2069,7 +2543,7 @@ }; var r = []; - var escape = false; + var escape = false, padNext = false; var hours = d.getUTCHours(); var isAM = hours < 12; if (monthNames == null) @@ -2097,9 +2571,15 @@ case 'b': c = "" + monthNames[d.getUTCMonth()]; break; case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; + case '0': c = ""; padNext = true; break; + } + if (c && padNext) { + c = leftPad(c); + padNext = false; } r.push(c); - escape = false; + if (!padNext) + escape = false; } else { if (c == "%") diff --git a/houseagent/templates/js/jquery.flot.selection.js b/houseagent/templates/js/jquery.flot.selection.js new file mode 100755 index 0000000..7f7b326 --- /dev/null +++ b/houseagent/templates/js/jquery.flot.selection.js @@ -0,0 +1,344 @@ +/* +Flot plugin for selecting regions. + +The plugin defines the following options: + + selection: { + mode: null or "x" or "y" or "xy", + color: color + } + +Selection support is enabled by setting the mode to one of "x", "y" or +"xy". In "x" mode, the user will only be able to specify the x range, +similarly for "y" mode. For "xy", the selection becomes a rectangle +where both ranges can be specified. "color" is color of the selection +(if you need to change the color later on, you can get to it with +plot.getOptions().selection.color). + +When selection support is enabled, a "plotselected" event will be +emitted on the DOM element you passed into the plot function. The +event handler gets a parameter with the ranges selected on the axes, +like this: + + placeholder.bind("plotselected", function(event, ranges) { + alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) + // similar for yaxis - with multiple axes, the extra ones are in + // x2axis, x3axis, ... + }); + +The "plotselected" event is only fired when the user has finished +making the selection. A "plotselecting" event is fired during the +process with the same parameters as the "plotselected" event, in case +you want to know what's happening while it's happening, + +A "plotunselected" event with no arguments is emitted when the user +clicks the mouse to remove the selection. + +The plugin allso adds the following methods to the plot object: + +- setSelection(ranges, preventEvent) + + Set the selection rectangle. The passed in ranges is on the same + form as returned in the "plotselected" event. If the selection mode + is "x", you should put in either an xaxis range, if the mode is "y" + you need to put in an yaxis range and both xaxis and yaxis if the + selection mode is "xy", like this: + + setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); + + setSelection will trigger the "plotselected" event when called. If + you don't want that to happen, e.g. if you're inside a + "plotselected" handler, pass true as the second parameter. If you + are using multiple axes, you can specify the ranges on any of those, + e.g. as x2axis/x3axis/... instead of xaxis, the plugin picks the + first one it sees. + +- clearSelection(preventEvent) + + Clear the selection rectangle. Pass in true to avoid getting a + "plotunselected" event. + +- getSelection() + + Returns the current selection in the same format as the + "plotselected" event. If there's currently no selection, the + function returns null. + +*/ + +(function ($) { + function init(plot) { + var selection = { + first: { x: -1, y: -1}, second: { x: -1, y: -1}, + show: false, + active: false + }; + + // FIXME: The drag handling implemented here should be + // abstracted out, there's some similar code from a library in + // the navigation plugin, this should be massaged a bit to fit + // the Flot cases here better and reused. Doing this would + // make this plugin much slimmer. + var savedhandlers = {}; + + var mouseUpHandler = null; + + function onMouseMove(e) { + if (selection.active) { + updateSelection(e); + + plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); + } + } + + function onMouseDown(e) { + if (e.which != 1) // only accept left-click + return; + + // cancel out any text selections + document.body.focus(); + + // prevent text selection and drag in old-school browsers + if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { + savedhandlers.onselectstart = document.onselectstart; + document.onselectstart = function () { return false; }; + } + if (document.ondrag !== undefined && savedhandlers.ondrag == null) { + savedhandlers.ondrag = document.ondrag; + document.ondrag = function () { return false; }; + } + + setSelectionPos(selection.first, e); + + selection.active = true; + + // this is a bit silly, but we have to use a closure to be + // able to whack the same handler again + mouseUpHandler = function (e) { onMouseUp(e); }; + + $(document).one("mouseup", mouseUpHandler); + } + + function onMouseUp(e) { + mouseUpHandler = null; + + // revert drag stuff for old-school browsers + if (document.onselectstart !== undefined) + document.onselectstart = savedhandlers.onselectstart; + if (document.ondrag !== undefined) + document.ondrag = savedhandlers.ondrag; + + // no more dragging + selection.active = false; + updateSelection(e); + + if (selectionIsSane()) + triggerSelectedEvent(); + else { + // this counts as a clear + plot.getPlaceholder().trigger("plotunselected", [ ]); + plot.getPlaceholder().trigger("plotselecting", [ null ]); + } + + return false; + } + + function getSelection() { + if (!selectionIsSane()) + return null; + + var r = {}, c1 = selection.first, c2 = selection.second; + $.each(plot.getAxes(), function (name, axis) { + if (axis.used) { + var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); + r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; + } + }); + return r; + } + + function triggerSelectedEvent() { + var r = getSelection(); + + plot.getPlaceholder().trigger("plotselected", [ r ]); + + // backwards-compat stuff, to be removed in future + if (r.xaxis && r.yaxis) + plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); + } + + function clamp(min, value, max) { + return value < min ? min: (value > max ? max: value); + } + + function setSelectionPos(pos, e) { + var o = plot.getOptions(); + var offset = plot.getPlaceholder().offset(); + var plotOffset = plot.getPlotOffset(); + pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); + pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); + + if (o.selection.mode == "y") + pos.x = pos == selection.first ? 0 : plot.width(); + + if (o.selection.mode == "x") + pos.y = pos == selection.first ? 0 : plot.height(); + } + + function updateSelection(pos) { + if (pos.pageX == null) + return; + + setSelectionPos(selection.second, pos); + if (selectionIsSane()) { + selection.show = true; + plot.triggerRedrawOverlay(); + } + else + clearSelection(true); + } + + function clearSelection(preventEvent) { + if (selection.show) { + selection.show = false; + plot.triggerRedrawOverlay(); + if (!preventEvent) + plot.getPlaceholder().trigger("plotunselected", [ ]); + } + } + + // function taken from markings support in Flot + function extractRange(ranges, coord) { + var axis, from, to, key, axes = plot.getAxes(); + + for (var k in axes) { + axis = axes[k]; + if (axis.direction == coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n == 1) + key = coord + "axis"; // support x1axis as xaxis + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } + } + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } + + return { from: from, to: to, axis: axis }; + } + + function setSelection(ranges, preventEvent) { + var axis, range, o = plot.getOptions(); + + if (o.selection.mode == "y") { + selection.first.x = 0; + selection.second.x = plot.width(); + } + else { + range = extractRange(ranges, "x"); + + selection.first.x = range.axis.p2c(range.from); + selection.second.x = range.axis.p2c(range.to); + } + + if (o.selection.mode == "x") { + selection.first.y = 0; + selection.second.y = plot.height(); + } + else { + range = extractRange(ranges, "y"); + + selection.first.y = range.axis.p2c(range.from); + selection.second.y = range.axis.p2c(range.to); + } + + selection.show = true; + plot.triggerRedrawOverlay(); + if (!preventEvent && selectionIsSane()) + triggerSelectedEvent(); + } + + function selectionIsSane() { + var minSize = 5; + return Math.abs(selection.second.x - selection.first.x) >= minSize && + Math.abs(selection.second.y - selection.first.y) >= minSize; + } + + plot.clearSelection = clearSelection; + plot.setSelection = setSelection; + plot.getSelection = getSelection; + + plot.hooks.bindEvents.push(function(plot, eventHolder) { + var o = plot.getOptions(); + if (o.selection.mode != null) { + eventHolder.mousemove(onMouseMove); + eventHolder.mousedown(onMouseDown); + } + }); + + + plot.hooks.drawOverlay.push(function (plot, ctx) { + // draw selection + if (selection.show && selectionIsSane()) { + var plotOffset = plot.getPlotOffset(); + var o = plot.getOptions(); + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var c = $.color.parse(o.selection.color); + + ctx.strokeStyle = c.scale('a', 0.8).toString(); + ctx.lineWidth = 1; + ctx.lineJoin = "round"; + ctx.fillStyle = c.scale('a', 0.4).toString(); + + var x = Math.min(selection.first.x, selection.second.x), + y = Math.min(selection.first.y, selection.second.y), + w = Math.abs(selection.second.x - selection.first.x), + h = Math.abs(selection.second.y - selection.first.y); + + ctx.fillRect(x, y, w, h); + ctx.strokeRect(x, y, w, h); + + ctx.restore(); + } + }); + + plot.hooks.shutdown.push(function (plot, eventHolder) { + eventHolder.unbind("mousemove", onMouseMove); + eventHolder.unbind("mousedown", onMouseDown); + + if (mouseUpHandler) + $(document).unbind("mouseup", mouseUpHandler); + }); + + } + + $.plot.plugins.push({ + init: init, + options: { + selection: { + mode: null, // one of null, "x", "y" or "xy" + color: "#e8cfac" + } + }, + name: 'selection', + version: '1.1' + }); +})(jQuery); diff --git a/houseagent/templates/js/jquery.flot.tooltip.js b/houseagent/templates/js/jquery.flot.tooltip.js new file mode 100644 index 0000000..7ab8a57 --- /dev/null +++ b/houseagent/templates/js/jquery.flot.tooltip.js @@ -0,0 +1,12 @@ +/* + * jquery.flot.tooltip + * + * description: easy-to-use tooltips for Flot charts + * version: 0.5 + * author: Krzysztof Urbas @krzysu [myviews.pl] + * website: https://github.com/krzysu/flot.tooltip + * + * build on 2012-12-30 + * released under MIT License, 2012 +*/ +(function(a){var b={defaultOptions:{tooltip:!1,tooltipOpts:{content:"%s | X: %x | Y: %y",xDateFormat:null,yDateFormat:null,shifts:{x:10,y:20},defaultTheme:!0,onHover:function(a,b){}}},tipPosition:{x:0,y:0},init:function(c){var d=b;c.hooks.bindEvents.push(function(b,c){d.plotOptions=b.getOptions();if(d.plotOptions.tooltip===!1||typeof d.plotOptions.tooltip=="undefined")return;d.tooltipOptions=d.plotOptions.tooltipOpts;var e=d.getDomElement();a(b.getPlaceholder()).bind("plothover",function(a,b,c){if(c){var f;f=d.stringFormat(d.tooltipOptions.content,c),e.html(f).css({left:d.tipPosition.x+d.tooltipOptions.shifts.x,top:d.tipPosition.y+d.tooltipOptions.shifts.y}).show(),typeof d.tooltipOptions.onHover=="function"&&d.tooltipOptions.onHover(c,e)}else e.hide().html("")}),c.mousemove(d.onMouseMove)})},getDomElement:function(){var b;return a("#flotTip").length>0?b=a("#flotTip"):(b=a("
").attr("id","flotTip"),b.appendTo("body").hide().css({position:"absolute"}),this.tooltipOptions.defaultTheme&&b.css({background:"#fff","z-index":"100",padding:"0.4em 0.6em","border-radius":"0.5em","font-size":"0.8em",border:"1px solid #111"})),b},onMouseMove:function(a){var c={x:0,y:0};c.x=a.pageX,c.y=a.pageY,b.updateTooltipPosition(c)},updateTooltipPosition:function(a){this.tipPosition.x=a.x,this.tipPosition.y=a.y},stringFormat:function(a,b){var c=/%p\.{0,1}(\d{0,})/,d=/%s/,e=/%x\.{0,1}(\d{0,})/,f=/%y\.{0,1}(\d{0,})/;return typeof b.series.percent!="undefined"&&(a=this.adjustValPrecision(c,a,b.series.percent)),typeof b.series.label!="undefined"&&(a=a.replace(d,b.series.label)),this.isTimeMode("xaxis",b)&&this.isXDateFormat(b)&&(a=a.replace(e,this.timestampToDate(b.series.data[b.dataIndex][0],this.tooltipOptions.xDateFormat))),this.isTimeMode("yaxis",b)&&this.isYDateFormat(b)&&(a=a.replace(f,this.timestampToDate(b.series.data[b.dataIndex][1],this.tooltipOptions.yDateFormat))),typeof b.series.data[b.dataIndex][0]=="number"&&(a=this.adjustValPrecision(e,a,b.series.data[b.dataIndex][0])),typeof b.series.data[b.dataIndex][1]=="number"&&(a=this.adjustValPrecision(f,a,b.series.data[b.dataIndex][1])),typeof b.series.xaxis.tickFormatter!="undefined"&&(a=a.replace(e,b.series.xaxis.tickFormatter(b.series.data[b.dataIndex][0],b.series.xaxis))),typeof b.series.yaxis.tickFormatter!="undefined"&&(a=a.replace(f,b.series.yaxis.tickFormatter(b.series.data[b.dataIndex][1],b.series.yaxis))),a},isTimeMode:function(a,b){return typeof b.series[a].options.mode!="undefined"&&b.series[a].options.mode==="time"},isXDateFormat:function(a){return typeof this.tooltipOptions.xDateFormat!="undefined"&&this.tooltipOptions.xDateFormat!==null},isYDateFormat:function(a){return typeof this.tooltipOptions.yDateFormat!="undefined"&&this.tooltipOptions.yDateFormat!==null},timestampToDate:function(b,c){var d=new Date(b);return a.plot.formatDate(d,c)},adjustValPrecision:function(a,b,c){var d;return b.match(a)!=="null"&&RegExp.$1!==""&&(d=RegExp.$1,c=c.toFixed(d),b=b.replace(a,c)),b}};a.plot.plugins.push({init:b.init,options:b.defaultOptions,name:"tooltip",version:"0.5"})})(jQuery); \ No newline at end of file