From f690c269c4d1dc8fdbee73408c491175d164f8d4 Mon Sep 17 00:00:00 2001 From: Jakob Hirsch Date: Tue, 22 Mar 2011 11:35:24 +0100 Subject: [PATCH 1/4] use flot 0.7 (svn r324) fixes several issues, most important the plot() memory leak --- .../javascripts/flot/jquery.colorhelpers.js | 39 +- .../javascripts/flot/jquery.flot.crosshair.js | 56 +- .../frontend/javascripts/flot/jquery.flot.js | 480 +++++++++++------- .../javascripts/flot/jquery.flot.navigate.js | 154 +++--- .../javascripts/flot/jquery.flot.pie.js | 13 +- .../javascripts/flot/jquery.flot.selection.js | 46 +- 6 files changed, 468 insertions(+), 320 deletions(-) diff --git a/htdocs/frontend/javascripts/flot/jquery.colorhelpers.js b/htdocs/frontend/javascripts/flot/jquery.colorhelpers.js index fa44961..d3524d7 100644 --- a/htdocs/frontend/javascripts/flot/jquery.colorhelpers.js +++ b/htdocs/frontend/javascripts/flot/jquery.colorhelpers.js @@ -1,6 +1,6 @@ /* Plugin for jQuery for working with colors. * - * Version 1.0. + * Version 1.1. * * Inspiration from jQuery color animation plugin by John Resig. * @@ -13,15 +13,18 @@ * 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 = {}; +(function($) { + $.color = {}; // construct color object with some convenient chainable helpers - jQuery.color.make = function (r, g, b, a) { + $.color.make = function (r, g, b, a) { var o = {}; o.r = r || 0; o.g = g || 0; @@ -61,7 +64,7 @@ }; o.clone = function () { - return jQuery.color.make(o.r, o.b, o.g, o.a); + return $.color.make(o.r, o.b, o.g, o.a); }; return o.normalize(); @@ -69,7 +72,7 @@ // extract CSS color property from element, going up in the DOM // if it's "transparent" - jQuery.color.extract = function (elem, css) { + $.color.extract = function (elem, css) { var c; do { c = elem.css(css).toLowerCase(); @@ -78,19 +81,20 @@ if (c != '' && c != 'transparent') break; elem = elem.parent(); - } while (!jQuery.nodeName(elem.get(0), "body")); + } while (!$.nodeName(elem.get(0), "body")); // catch Safari's way of signalling transparent if (c == "rgba(0, 0, 0, 0)") c = "transparent"; - return jQuery.color.parse(c); + return $.color.parse(c); } // parse CSS color string (like "rgb(10, 32, 43)" or "#fff"), - // returns color object - jQuery.color.parse = function (str) { - var res, m = jQuery.color.make; + // returns color object, if parsing failed, you get black (0, 0, + // 0) out + $.color.parse = function (str) { + var res, m = $.color.make; // Look for rgb(num,num,num) if (res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)) @@ -117,11 +121,12 @@ return m(parseInt(res[1]+res[1], 16), parseInt(res[2]+res[2], 16), parseInt(res[3]+res[3], 16)); // Otherwise, we're most likely dealing with a named color - var name = jQuery.trim(str).toLowerCase(); + var name = $.trim(str).toLowerCase(); if (name == "transparent") return m(255, 255, 255, 0); else { - res = lookupColors[name]; + // default to black + res = lookupColors[name] || [0, 0, 0]; return m(res[0], res[1], res[2]); } } @@ -170,5 +175,5 @@ silver:[192,192,192], white:[255,255,255], yellow:[255,255,0] - }; -})(); + }; +})(jQuery); diff --git a/htdocs/frontend/javascripts/flot/jquery.flot.crosshair.js b/htdocs/frontend/javascripts/flot/jquery.flot.crosshair.js index 30b1ec9..1d433f0 100644 --- a/htdocs/frontend/javascripts/flot/jquery.flot.crosshair.js +++ b/htdocs/frontend/javascripts/flot/jquery.flot.crosshair.js @@ -90,34 +90,37 @@ The plugin also adds four public methods: crosshair.locked = false; } + function onMouseOut(e) { + if (crosshair.locked) + return; + + if (crosshair.x != -1) { + crosshair.x = -1; + plot.triggerRedrawOverlay(); + } + } + + function onMouseMove(e) { + if (crosshair.locked) + return; + + if (plot.getSelection && plot.getSelection()) { + crosshair.x = -1; // hide the crosshair while selecting + return; + } + + var offset = plot.offset(); + crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); + crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); + plot.triggerRedrawOverlay(); + } + plot.hooks.bindEvents.push(function (plot, eventHolder) { if (!plot.getOptions().crosshair.mode) return; - eventHolder.mouseout(function () { - if (crosshair.locked) - return; - - if (crosshair.x != -1) { - crosshair.x = -1; - plot.triggerRedrawOverlay(); - } - }); - - eventHolder.mousemove(function (e) { - if (crosshair.locked) - return; - - if (plot.getSelection && plot.getSelection()) { - crosshair.x = -1; // hide the crosshair while selecting - return; - } - - var offset = plot.offset(); - crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); - crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); - plot.triggerRedrawOverlay(); - }); + eventHolder.mouseout(onMouseOut); + eventHolder.mousemove(onMouseMove); }); plot.hooks.drawOverlay.push(function (plot, ctx) { @@ -148,6 +151,11 @@ The plugin also adds four public methods: } ctx.restore(); }); + + plot.hooks.shutdown.push(function (plot, eventHolder) { + eventHolder.unbind("mouseout", onMouseOut); + eventHolder.unbind("mousemove", onMouseMove); + }); } $.plot.plugins.push({ diff --git a/htdocs/frontend/javascripts/flot/jquery.flot.js b/htdocs/frontend/javascripts/flot/jquery.flot.js index 182f457..aabc544 100644 --- a/htdocs/frontend/javascripts/flot/jquery.flot.js +++ b/htdocs/frontend/javascripts/flot/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,6 +54,7 @@ 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 @@ -64,6 +68,7 @@ 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 @@ -119,6 +124,7 @@ labelMargin: 5, // in pixels axisMargin: 8, // in pixels borderWidth: 2, // in pixels + 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, @@ -145,7 +151,8 @@ drawSeries: [], draw: [], bindEvents: [], - drawOverlay: [] + drawOverlay: [], + shutdown: [] }, plot = this; @@ -165,30 +172,16 @@ return o; }; plot.getData = function () { return series; }; - plot.getAxis = function (dir, number) { - var a = (dir == x ? xaxes : yaxes)[number - 1]; - if (a && !a.used) - a = null; - return a; - }; plot.getAxes = function () { var res = {}, i; - for (i = 0; i < xaxes.length; ++i) - res["x" + (i ? (i + 1) : "") + "axis"] = xaxes[i] || {}; - for (i = 0; i < yaxes.length; ++i) - res["y" + (i ? (i + 1) : "") + "axis"] = yaxes[i] || {}; - - // backwards compatibility - to be removed - if (!res.x2axis) - res.x2axis = { n: 2 }; - if (!res.y2axis) - res.y2axis = { n: 2 }; - + $.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.getUsedAxes = getUsedAxes; // return flat array with x and y axes that are in use plot.c2p = canvasToAxisCoords; plot.p2c = axisToCanvasCoords; plot.getOptions = function () { return options; }; @@ -201,6 +194,12 @@ 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; @@ -208,7 +207,7 @@ // initialize initPlugins(plot); parseOptions(options_); - constructCanvas(); + setupCanvases(); setData(data_); setupGrid(); draw(); @@ -256,20 +255,19 @@ 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.x2axis.position = "top"; - options.xaxes[1] = options.x2axis; + options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); + options.xaxes[1].position = "top"; } if (options.y2axis) { - if (options.y2axis.autoscaleMargin === undefined) - options.y2axis.autoscaleMargin = 0.02; - options.y2axis.position = "right"; - options.yaxes[1] = 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; @@ -281,9 +279,10 @@ $.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) @@ -308,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; @@ -333,6 +332,11 @@ 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; @@ -367,7 +371,7 @@ if (pos[key] == null && axis.n == 1) key = "x"; - if (pos[key]) { + if (pos[key] != null) { res.left = axis.p2c(pos[key]); break; } @@ -381,7 +385,7 @@ if (pos[key] == null && axis.n == 1) key = "y"; - if (pos[key]) { + if (pos[key] != null) { res.top = axis.p2c(pos[key]); break; } @@ -391,21 +395,6 @@ return res; } - function getUsedAxes() { - var res = [], i, axis; - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) - res.push(axis); - } - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) - res.push(axis); - } - return res; - } - function getOrCreateAxis(axes, number) { if (!axes[number - 1]) axes[number - 1] = { @@ -500,29 +489,23 @@ 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; - function initAxis(axis, number) { - if (!axis) - return; - - axis.datamin = topSentry; - axis.datamax = bottomSentry; - 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; } - for (i = 0; i < xaxes.length; ++i) - initAxis(xaxes[i]); - for (i = 0; i < yaxes.length; ++i) - initAxis(yaxes[i]); + $.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]; @@ -579,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) { @@ -653,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) { @@ -688,7 +675,7 @@ updateAxis(s.yaxis, ymin, ymax); } - $.each(getUsedAxes(), function (i, axis) { + $.each(allAxes(), function (_, axis) { if (axis.datamin == topSentry) axis.datamin = null; if (axis.datamax == bottomSentry) @@ -696,46 +683,115 @@ }); } - function constructCanvas() { - function makeCanvas(width, height) { - var c = document.createElement('canvas'); - c.width = width; - c.height = height; - if (!c.getContext) // 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(); - 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; - - if (window.G_vmlCanvasManager) // excanvas hack - window.G_vmlCanvasManager.init_(document); // make sure everything is setup - - // the canvas - canvas = $(makeCanvas(canvasWidth, canvasHeight)).appendTo(placeholder).get(0); - ctx = canvas.getContext("2d"); - - // overlay canvas for interactive features - overlay = $(makeCanvas(canvasWidth, canvasHeight)).css({ position: 'absolute', left: 0, top: 0 }).appendTo(placeholder).get(0); - octx = overlay.getContext("2d"); - octx.stroke(); + return c; } - function bindEvents() { + function getCanvasDimensions() { + canvasWidth = placeholder.width(); + canvasHeight = placeholder.height(); + + 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; + + // 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 + + placeholder.css({ padding: 0 }); // padding messes up the positioning + + if (placeholder.css("position") == 'static') + placeholder.css("position", "relative"); // for positioning labels and overlay + + 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"); + // 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); @@ -743,6 +799,17 @@ executeHooks(hooks.bindEvents, [eventHolder]); } + 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 @@ -752,42 +819,31 @@ 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") { - // 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); }; + 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 / (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); }; + s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); + s = -s; + m = Math.max(t(axis.max), 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); }; } function measureTickLabels(axis) { - if (!axis) - return; - var opts = axis.options, i, ticks = axis.ticks || [], labels = [], l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv; @@ -847,15 +903,12 @@ w = 0; if (h == null) h = 0; - + axis.labelWidth = w; axis.labelHeight = h; } - function computeAxisBox(axis) { - if (!axis || (!axis.used && !(axis.labelWidth || axis.labelHeight))) - return; - + function allocateAxisBoxFirstPhase(axis) { // find the bounding box of the axis by looking at label // widths/heights and ticks, make room by diminishing the // plotOffset @@ -871,7 +924,7 @@ // determine axis margin var samePosition = $.grep(all, function (a) { - return a && a.options.position == pos && (a.labelHeight || a.labelWidth); + return a && a.options.position == pos && a.reserveSpace; }); if ($.inArray(axis, samePosition) == samePosition.length - 1) axismargin = 0; // outermost @@ -881,7 +934,7 @@ tickLength = "full"; var sameDirection = $.grep(all, function (a) { - return a && (a.labelHeight || a.labelWidth); + return a && a.reserveSpace; }); var innermost = $.inArray(axis, sameDirection) == 0; @@ -924,7 +977,7 @@ axis.innermost = innermost; } - function fixupAxisBox(axis) { + function allocateAxisBoxSecondPhase(axis) { // set remaining bounding box coordinates if (axis.direction == "x") { axis.box.left = plotOffset.left; @@ -937,59 +990,67 @@ } function setupGrid() { - var axes = getUsedAxes(), j, k; + var i, axes = allAxes(); - // compute axis intervals - for (k = 0; k < axes.length; ++k) - setRange(axes[k]); + // 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) { - // make the ticks - for (k = 0; k < axes.length; ++k) { - setupTickGeneration(axes[k]); - setTicks(axes[k]); - snapRangeToTicks(axes[k], axes[k].ticks); - } + $.each(allocatedAxes, function (_, axis) { + // make the ticks + setupTickGeneration(axis); + setTicks(axis); + snapRangeToTicks(axis, axis.ticks); - // find labelWidth/Height, do this on all, not just - // used as we might need to reserve space for unused - // too if their labelWidth/Height is set - for (j = 0; j < xaxes.length; ++j) - measureTickLabels(xaxes[j]); - for (j = 0; j < yaxes.length; ++j) - measureTickLabels(yaxes[j]); - - // compute the axis boxes, start from the outside (reverse order) - for (j = xaxes.length - 1; j >= 0; --j) - computeAxisBox(xaxes[j]); - for (j = yaxes.length - 1; j >= 0; --j) - computeAxisBox(yaxes[j]); + // 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 maxOutset = 0; - for (var i = 0; i < series.length; ++i) - maxOutset = Math.max(maxOutset, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); - + 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(maxOutset, plotOffset[a]); + plotOffset[a] = Math.max(minMargin, plotOffset[a]); } } plotWidth = canvasWidth - plotOffset.left - plotOffset.right; plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top; - + // now we got the proper plotWidth/Height, we can compute the scaling - for (k = 0; k < axes.length; ++k) - setTransformationHelpers(axes[k]); + $.each(axes, function (_, axis) { + setTransformationHelpers(axis); + }); if (options.grid.show) { - for (k = 0; k < axes.length; ++k) - fixupAxisBox(axes[k]); - + $.each(allocatedAxes, function (_, axis) { + allocateAxisBoxSecondPhase(axis); + }); + insertAxisLabels(); } @@ -1008,7 +1069,7 @@ 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 (opts.max == null || opts.min != null) max += widen; @@ -1042,12 +1103,10 @@ var noTicks; if (typeof opts.ticks == "number" && opts.ticks > 0) noTicks = opts.ticks; - else if (axis.direction == "x") - // heuristic based on the model a*sqrt(x) fitted to - // some reasonable data points - noTicks = 0.3 * Math.sqrt(canvasWidth); 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; @@ -1310,9 +1369,7 @@ } function setTicks(axis) { - axis.ticks = []; - - var oticks = axis.options.ticks, ticks = null; + var oticks = axis.options.ticks, ticks = []; if (oticks == null || (typeof oticks == "number" && oticks > 0)) ticks = axis.tickGenerator(axis); else if (oticks) { @@ -1325,24 +1382,26 @@ // 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]; + v = +t[0]; if (t.length > 1) label = t[1]; } else - v = t; + v = +t; if (label == null) label = axis.tickFormatter(v, axis); - axis.ticks[i] = { v: v, label: label }; + if (!isNaN(v)) + axis.ticks.push({ v: v, label: label }); } } function snapRangeToTicks(axis, ticks) { - if (axis.options.autoscaleMargin != null && ticks.length > 0) { + if (axis.options.autoscaleMargin && ticks.length > 0) { // snap to ticks if (axis.options.min == null) axis.min = Math.min(axis.min, ticks[0].v); @@ -1355,6 +1414,10 @@ 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(); @@ -1371,9 +1434,8 @@ } function extractRange(ranges, coord) { - var axis, from, to, axes, key; + var axis, from, to, key, axes = allAxes(); - axes = getUsedAxes(); for (i = 0; i < axes.length; ++i) { axis = axes[i]; if (axis.direction == coord) { @@ -1405,18 +1467,21 @@ 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) { @@ -1486,15 +1551,17 @@ } // draw the ticks - var axes = getUsedAxes(), bw = options.grid.borderWidth; + 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; @@ -1588,9 +1655,11 @@ var html = ['
']; - var axes = getUsedAxes(); + 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) { @@ -2198,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]; @@ -2264,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; }); @@ -2294,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); } @@ -2447,6 +2531,8 @@ return plot; }; + $.plot.version = "0.7"; + $.plot.plugins = []; // returns a string with the date d formatted according to fmt diff --git a/htdocs/frontend/javascripts/flot/jquery.flot.navigate.js b/htdocs/frontend/javascripts/flot/jquery.flot.navigate.js index 7e33efa..f2b9760 100644 --- a/htdocs/frontend/javascripts/flot/jquery.flot.navigate.js +++ b/htdocs/frontend/javascripts/flot/jquery.flot.navigate.js @@ -17,12 +17,13 @@ Options: pan: { interactive: false + cursor: "move" // CSS mouse cursor value used when dragging, e.g. "pointer" frameRate: 20 } xaxis, yaxis, x2axis, y2axis: { - zoomRange: null // or [number, number] (min range, max range) - panRange: null // or [number, number] (min, max) + zoomRange: null // or [number, number] (min range, max range) or false + panRange: null // or [number, number] (min, max) or false } "interactive" enables the built-in drag/click behaviour. If you enable @@ -32,6 +33,9 @@ moving around; the same for zoom. "amount" specifies the default amount to zoom in (so 1.5 = 150%) relative to the current viewport. +"cursor" is a standard CSS mouse cursor string used for visual +feedback to the user when dragging. + "frameRate" specifies the maximum number of times per second the plot will update itself while the user is panning around on it (set to null to disable intermediate pans, the plot will then not update until the @@ -40,11 +44,13 @@ mouse button is released). "zoomRange" is the interval in which zooming can happen, e.g. with zoomRange: [1, 100] the zoom will never scale the axis so that the difference between min and max is smaller than 1 or larger than 100. -You can set either end to null to ignore, e.g. [1, null]. +You can set either end to null to ignore, e.g. [1, null]. If you set +zoomRange to false, zooming on that axis will be disabled. "panRange" confines the panning to stay within a range, e.g. with panRange: [-10, 20] panning stops at -10 in one end and at 20 in the -other. Either can be null, e.g. [-10, null]. +other. Either can be null, e.g. [-10, null]. If you set +panRange to false, panning on that axis will be disabled. Example API usage: @@ -114,66 +120,78 @@ Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-L }, pan: { interactive: false, + cursor: "move", frameRate: 20 } }; function init(plot) { + function onZoomClick(e, zoomOut) { + var c = plot.offset(); + c.left = e.pageX - c.left; + c.top = e.pageY - c.top; + if (zoomOut) + plot.zoomOut({ center: c }); + else + plot.zoom({ center: c }); + } + + function onMouseWheel(e, delta) { + onZoomClick(e, delta < 0); + return false; + } + + var prevCursor = 'default', prevPageX = 0, prevPageY = 0, + panTimeout = null; + + function onDragStart(e) { + if (e.which != 1) // only accept left-click + return false; + var c = plot.getPlaceholder().css('cursor'); + if (c) + prevCursor = c; + plot.getPlaceholder().css('cursor', plot.getOptions().pan.cursor); + prevPageX = e.pageX; + prevPageY = e.pageY; + } + + function onDrag(e) { + var frameRate = plot.getOptions().pan.frameRate; + if (panTimeout || !frameRate) + return; + + panTimeout = setTimeout(function () { + plot.pan({ left: prevPageX - e.pageX, + top: prevPageY - e.pageY }); + prevPageX = e.pageX; + prevPageY = e.pageY; + + panTimeout = null; + }, 1 / frameRate * 1000); + } + + function onDragEnd(e) { + if (panTimeout) { + clearTimeout(panTimeout); + panTimeout = null; + } + + plot.getPlaceholder().css('cursor', prevCursor); + plot.pan({ left: prevPageX - e.pageX, + top: prevPageY - e.pageY }); + } + function bindEvents(plot, eventHolder) { var o = plot.getOptions(); if (o.zoom.interactive) { - function clickHandler(e, zoomOut) { - var c = plot.offset(); - c.left = e.pageX - c.left; - c.top = e.pageY - c.top; - if (zoomOut) - plot.zoomOut({ center: c }); - else - plot.zoom({ center: c }); - } - - eventHolder[o.zoom.trigger](clickHandler); - - eventHolder.mousewheel(function (e, delta) { - clickHandler(e, delta < 0); - return false; - }); + eventHolder[o.zoom.trigger](onZoomClick); + eventHolder.mousewheel(onMouseWheel); } - if (o.pan.interactive) { - var prevCursor = 'default', pageX = 0, pageY = 0, - panTimeout = null; - - eventHolder.bind("dragstart", { distance: 10 }, function (e) { - if (e.which != 1) // only accept left-click - return false; - eventHolderCursor = eventHolder.css('cursor'); - eventHolder.css('cursor', 'move'); - pageX = e.pageX; - pageY = e.pageY; - }); - eventHolder.bind("drag", function (e) { - if (panTimeout || !o.pan.frameRate) - return; - panTimeout = setTimeout(function () { - plot.pan({ left: pageX - e.pageX, - top: pageY - e.pageY }); - pageX = e.pageX; - pageY = e.pageY; - - panTimeout = null; - }, 1/o.pan.frameRate * 1000); - }); - eventHolder.bind("dragend", function (e) { - if (panTimeout) { - clearTimeout(panTimeout); - panTimeout = null; - } - - eventHolder.css('cursor', prevCursor); - plot.pan({ left: pageX - e.pageX, - top: pageY - e.pageY }); - }); + if (o.pan.interactive) { + eventHolder.bind("dragstart", { distance: 10 }, onDragStart); + eventHolder.bind("drag", onDrag); + eventHolder.bind("dragend", onDragEnd); } } @@ -212,10 +230,14 @@ Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-L } }; - $.each(plot.getUsedAxes(), function(i, axis) { + $.each(plot.getAxes(), function(_, axis) { var opts = axis.options, min = minmax[axis.direction].min, - max = minmax[axis.direction].max + max = minmax[axis.direction].max, + zr = opts.zoomRange; + + if (zr === false) // no zooming on this axis + return; min = axis.c2p(min); max = axis.c2p(max); @@ -226,7 +248,7 @@ Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-L max = tmp; } - var range = max - min, zr = opts.zoomRange; + var range = max - min; if (zr && ((zr[0] != null && range < zr[0]) || (zr[1] != null && range > zr[1]))) @@ -254,7 +276,7 @@ Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-L if (isNaN(delta.y)) delta.y = 0; - $.each(plot.getUsedAxes(), function (i, axis) { + $.each(plot.getAxes(), function (_, axis) { var opts = axis.options, min, max, d = delta[axis.direction]; @@ -262,6 +284,9 @@ Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-L max = axis.c2p(axis.p2c(axis.max) + d); var pr = opts.panRange; + if (pr === false) // no panning on this axis + return; + if (pr) { // check whether we hit the wall if (pr[0] != null && pr[0] > min) { @@ -287,14 +312,25 @@ Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-L if (!args.preventEvent) plot.getPlaceholder().trigger("plotpan", [ plot ]); } + + function shutdown(plot, eventHolder) { + eventHolder.unbind(plot.getOptions().zoom.trigger, onZoomClick); + eventHolder.unbind("mousewheel", onMouseWheel); + eventHolder.unbind("dragstart", onDragStart); + eventHolder.unbind("drag", onDrag); + eventHolder.unbind("dragend", onDragEnd); + if (panTimeout) + clearTimeout(panTimeout); + } plot.hooks.bindEvents.push(bindEvents); + plot.hooks.shutdown.push(shutdown); } $.plot.plugins.push({ init: init, options: options, name: 'navigate', - version: '1.2' + version: '1.3' }); })(jQuery); diff --git a/htdocs/frontend/javascripts/flot/jquery.flot.pie.js b/htdocs/frontend/javascripts/flot/jquery.flot.pie.js index ed01cf2..70941dd 100644 --- a/htdocs/frontend/javascripts/flot/jquery.flot.pie.js +++ b/htdocs/frontend/javascripts/flot/jquery.flot.pie.js @@ -403,7 +403,7 @@ More detail and specific examples can be found in the included HTML file. } ctx.beginPath(); - if (angle!=Math.PI*2) + if (Math.abs(angle - Math.PI*2) > 0.000000001) ctx.moveTo(0,0); // Center of the pie else if ($.browser.msie) angle -= 0.0001; @@ -607,12 +607,9 @@ More detail and specific examples can be found in the included HTML file. } } - // if no slice was found, quit - if (!item) - return; - // highlight the slice - highlight(item.series, eventname); + if (item) + highlight(item.series, eventname); // trigger any hover bind events var pos = { pageX: e.pageX, pageY: e.pageY }; @@ -691,7 +688,7 @@ More detail and specific examples can be found in the included HTML file. octx.fillStyle = "rgba(255, 255, 255, "+options.series.pie.highlight.opacity+")"; // this is temporary until we have access to parseColor octx.beginPath(); - if (series.angle!=Math.PI*2) + if (Math.abs(series.angle - Math.PI*2) > 0.000000001) octx.moveTo(0,0); // Center of the pie octx.arc(0,0,radius,series.startAngle,series.startAngle+series.angle,false); octx.closePath(); @@ -750,4 +747,4 @@ More detail and specific examples can be found in the included HTML file. name: "pie", version: "1.0" }); -})(jQuery); \ No newline at end of file +})(jQuery); diff --git a/htdocs/frontend/javascripts/flot/jquery.flot.selection.js b/htdocs/frontend/javascripts/flot/jquery.flot.selection.js index 1179a13..7f7b326 100644 --- a/htdocs/frontend/javascripts/flot/jquery.flot.selection.js +++ b/htdocs/frontend/javascripts/flot/jquery.flot.selection.js @@ -11,7 +11,9 @@ The plugin defines the following options: 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. +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 @@ -79,11 +81,13 @@ The plugin allso adds the following methods to the plot object: // make this plugin much slimmer. var savedhandlers = {}; + var mouseUpHandler = null; + function onMouseMove(e) { if (selection.active) { - plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); - updateSelection(e); + + plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); } } @@ -107,18 +111,24 @@ The plugin allso adds the following methods to the plot object: 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", onMouseUp); + $(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 draggy-dee-drag + // no more dragging selection.active = false; updateSelection(e); @@ -197,13 +207,12 @@ The plugin allso adds the following methods to the plot object: } } - // taken from markings support + // function taken from markings support in Flot function extractRange(ranges, coord) { - var axis, from, to, axes, key; + var axis, from, to, key, axes = plot.getAxes(); - axes = plot.getUsedAxes(); - for (i = 0; i < axes.length; ++i) { - axis = axes[i]; + for (var k in axes) { + axis = axes[k]; if (axis.direction == coord) { key = coord + axis.n + "axis"; if (!ranges[key] && axis.n == 1) @@ -233,7 +242,6 @@ The plugin allso adds the following methods to the plot object: return { from: from, to: to, axis: axis }; } - function setSelection(ranges, preventEvent) { var axis, range, o = plot.getOptions(); @@ -277,11 +285,10 @@ The plugin allso adds the following methods to the plot object: plot.hooks.bindEvents.push(function(plot, eventHolder) { var o = plot.getOptions(); - if (o.selection.mode != null) + if (o.selection.mode != null) { eventHolder.mousemove(onMouseMove); - - if (o.selection.mode != null) eventHolder.mousedown(onMouseDown); + } }); @@ -312,6 +319,15 @@ The plugin allso adds the following methods to the plot object: 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({ @@ -323,6 +339,6 @@ The plugin allso adds the following methods to the plot object: } }, name: 'selection', - version: '1.0' + version: '1.1' }); })(jQuery); From 804a01f6cbe15ab69b3c54b26338b12320312d20 Mon Sep 17 00:00:00 2001 From: Jakob Hirsch Date: Tue, 22 Mar 2011 11:36:57 +0100 Subject: [PATCH 2/4] use custom RewriteBase --- htdocs/.htaccess | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/.htaccess b/htdocs/.htaccess index 98ded0f..dbc3d5a 100644 --- a/htdocs/.htaccess +++ b/htdocs/.htaccess @@ -3,4 +3,5 @@ Allow from all RewriteEngine On RewriteRule ^backend/(.*) backend.php/$1 [L] + RewriteBase /vz From ee788774ec0b2f3d886dc399403bd1a23dff49e8 Mon Sep 17 00:00:00 2001 From: Jakob Hirsch Date: Tue, 22 Mar 2011 12:54:35 +0100 Subject: [PATCH 3/4] reworked auto-refresh - re-calculate refresh time after zooming or selecting - calculate refresh time according to number of tuples - use wrappers for setting/clearing refresh time --- htdocs/frontend/index.html | 2 +- htdocs/frontend/javascripts/options.js | 1 + htdocs/frontend/javascripts/wui.js | 46 +++++++++++++++++--------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/htdocs/frontend/index.html b/htdocs/frontend/index.html index 43f4db9..529f047 100644 --- a/htdocs/frontend/index.html +++ b/htdocs/frontend/index.html @@ -99,7 +99,7 @@ - + Darstellung diff --git a/htdocs/frontend/javascripts/options.js b/htdocs/frontend/javascripts/options.js index a68f77b..dba9021 100644 --- a/htdocs/frontend/javascripts/options.js +++ b/htdocs/frontend/javascripts/options.js @@ -32,6 +32,7 @@ vz.options = { precision: 2, // TODO update from backend capabilities? render: 'lines', refresh: false, + minTimeout: 3000, // minimum refresh time in ms defaultInterval: 24*60*60*1000, // 1 day timezoneOffset: -(new Date().getTimezoneOffset() * 60000) // TODO add option with timezone dropdown }; diff --git a/htdocs/frontend/javascripts/wui.js b/htdocs/frontend/javascripts/wui.js index 96e257e..602ba83 100644 --- a/htdocs/frontend/javascripts/wui.js +++ b/htdocs/frontend/javascripts/wui.js @@ -28,6 +28,7 @@ * Initialize the WUI (Web User Interface) */ vz.wui.init = function() { + vz.wui.timeout = null; // initialize dropdown accordion $('#accordion h3').click(function() { $(this).next().toggle('fast'); @@ -81,21 +82,14 @@ vz.wui.init = function() { // auto refresh if (vz.options.refresh) { $('#refresh').attr('checked', true); - - var delta = vz.options.plot.xaxis.max - vz.options.plot.xaxis.min; - this.timeout = window.setTimeout( - this.refresh, - (delta / 100 < 3000) ? 3000 : delta / 100 - ); + vz.wui.setTimeout(); } $('#refresh').change(function() { vz.options.refresh = $(this).attr('checked'); - if (vz.options.refresh) { - vz.wui.timeout = window.setTimeout(vz.wui.refresh, 3000); - } - else { - window.clearTimeout(this.timeout); + vz.wui.setTimeout(); + } else { + vz.wui.clearTimeout(); } }); @@ -252,13 +246,32 @@ vz.wui.refresh = function() { vz.options.plot.xaxis.min = vz.options.plot.xaxis.max - delta; // move plot vz.entities.loadData().done(function() { vz.wui.drawPlot(); - vz.wui.timeout = window.setTimeout( // restart refresh timeout - vz.wui.refresh, // self - (delta / 100 < 3000) ? 3000 : delta / 100 - ); }); }; +/** + * refresh graphs after timeout ms, with a minimum f vz.options.minTimeout ms + */ +vz.wui.setTimeout = function() { + // clear an already set timeout + if (vz.wui.timeout != null) { + window.clearTimeout(vz.wui.timeout); + } + var t = Math.max((vz.options.plot.xaxis.max - vz.options.plot.xaxis.min)/vz.options.tuples, vz.options.minTimeout); + $('#refresh-time').html(Math.round(t/1000)+"s"); + vz.wui.timeout = window.setTimeout(this.refresh, t); +} + +/** + * stop auto-refresh of graphs + */ +vz.wui.clearTimeout = function() { + $('#refresh-time').html(''); + var rc = window.clearTimeout(vz.wui.timeout); + vz.wui.timeout = null; + return rc; +} + /** * Move & zoom in the plotting area */ @@ -536,6 +549,9 @@ vz.wui.drawPlot = function () { } vz.plot = $.plot($('#flot'), series, vz.options.plot); + if (vz.options.refresh) { + vz.wui.setTimeout(); + } }; /* From 00c24ec2d1102847343c75028174433eacd7aabc Mon Sep 17 00:00:00 2001 From: Jakob Hirsch Date: Tue, 22 Mar 2011 15:16:56 +0100 Subject: [PATCH 4/4] don't auto-refresh if we want to see old data --- htdocs/frontend/javascripts/wui.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/htdocs/frontend/javascripts/wui.js b/htdocs/frontend/javascripts/wui.js index 602ba83..f43d474 100644 --- a/htdocs/frontend/javascripts/wui.js +++ b/htdocs/frontend/javascripts/wui.js @@ -256,7 +256,16 @@ vz.wui.setTimeout = function() { // clear an already set timeout if (vz.wui.timeout != null) { window.clearTimeout(vz.wui.timeout); + vz.wui.timeout = null; } + // don't refresh if the end of the x axis is not the current time, i.e. we are looking at some old data + // we allow an offset of 1s, because loading data takes some time. this also means that if it takes more than 1s, + // we will not automatically refresh. this is a feature! + if (vz.options.plot.xaxis.max < Number(new Date()) - 1000) { + $('#refresh-time').html('(deactivated)'); + return; + } + var t = Math.max((vz.options.plot.xaxis.max - vz.options.plot.xaxis.min)/vz.options.tuples, vz.options.minTimeout); $('#refresh-time').html(Math.round(t/1000)+"s"); vz.wui.timeout = window.setTimeout(this.refresh, t); @@ -334,14 +343,14 @@ vz.wui.handleControls = function () { // reenable autoscaling for yaxis vz.options.plot.yaxis.max = null; // autoscaling vz.options.plot.yaxis.min = 0; // fixed to 0 - + // we dont want to zoom/pan into the future if (vz.options.plot.xaxis.max > new Date().getTime()) { delta = vz.options.plot.xaxis.max - vz.options.plot.xaxis.min; vz.options.plot.xaxis.max = new Date().getTime(); vz.options.plot.xaxis.min = new Date().getTime() - delta; } - + vz.entities.loadData().done(vz.wui.drawPlot); };