diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html index f4969418..cd46ce55 100644 --- a/_includes/recline-deps.html +++ b/_includes/recline-deps.html @@ -29,6 +29,7 @@ + diff --git a/docs/tutorial-views.markdown b/docs/tutorial-views.markdown index 550f351e..0a4a40be 100644 --- a/docs/tutorial-views.markdown +++ b/docs/tutorial-views.markdown @@ -130,6 +130,7 @@ library and the Recline Flot Graph view: + {% endhighlight %} diff --git a/src/view.flot.js b/src/view.flot.js index 7bebbf93..5059156e 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -155,25 +155,17 @@ my.Flot = Backbone.View.extend({ }, _xaxisLabel: function (x) { - var xfield = this.model.fields.get(this.state.attributes.group); - - // time series - var xtype = xfield.get('type'); - var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time'); - - if (this.xvaluesAreIndex) { + if (this._groupFieldIsDateTime()) { + // oddly x comes through as milliseconds *string* (rather than int + // or float) so we have to reparse + x = new Date(parseFloat(x)).toLocaleDateString(); + } else if (this.xvaluesAreIndex) { x = parseInt(x, 10); // HACK: deal with bar graph style cases where x-axis items were strings // In this case x at this point is the index of the item in the list of // records not its actual x-axis value x = this.model.records.models[x].get(this.state.attributes.group); } - if (isDateTime) { - x = new Date(x).toLocaleDateString(); - } - // } else if (isDateTime) { - // x = new Date(parseInt(x, 10)).toLocaleDateString(); - // } return x; }, @@ -188,25 +180,26 @@ my.Flot = Backbone.View.extend({ // @param numPoints the number of points that will be plotted getGraphOptions: function(typeId, numPoints) { var self = this; - - var tickFormatter = function (x) { - // convert x to a string and make sure that it is not too long or the - // tick labels will overlap - // TODO: find a more accurate way of calculating the size of tick labels - var label = self._xaxisLabel(x) || ""; - - if (typeof label !== 'string') { - label = label.toString(); - } - if (self.state.attributes.graphType !== 'bars' && label.length > 10) { - label = label.slice(0, 10) + "..."; - } - - return label; - }; - + var groupFieldIsDateTime = self._groupFieldIsDateTime(); var xaxis = {}; - xaxis.tickFormatter = tickFormatter; + + if (!groupFieldIsDateTime) { + xaxis.tickFormatter = function (x) { + // convert x to a string and make sure that it is not too long or the + // tick labels will overlap + // TODO: find a more accurate way of calculating the size of tick labels + var label = self._xaxisLabel(x) || ""; + + if (typeof label !== 'string') { + label = label.toString(); + } + if (self.state.attributes.graphType !== 'bars' && label.length > 10) { + label = label.slice(0, 10) + "..."; + } + + return label; + }; + } // for labels case we only want ticks at the label intervals // HACK: however we also get this case with Date fields. In that case we @@ -219,6 +212,8 @@ my.Flot = Backbone.View.extend({ ticks.push(parseInt(i*increment, 10)); } xaxis.ticks = ticks; + } else if (groupFieldIsDateTime) { + xaxis.mode = 'time'; } var yaxis = {}; @@ -300,24 +295,31 @@ my.Flot = Backbone.View.extend({ } }, + _groupFieldIsDateTime: function() { + var xfield = this.model.fields.get(this.state.attributes.group); + var xtype = xfield.get('type'); + var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time'); + return isDateTime; + }, + createSeries: function() { var self = this; self.xvaluesAreIndex = false; var series = []; + var xfield = self.model.fields.get(self.state.attributes.group); + var isDateTime = self._groupFieldIsDateTime(); + _.each(this.state.attributes.series, function(field) { var points = []; var fieldLabel = self.model.fields.get(field).get('label'); _.each(self.model.records.models, function(doc, index) { - var xfield = self.model.fields.get(self.state.attributes.group); var x = doc.getFieldValue(xfield); - // time series - var xtype = xfield.get('type'); - var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time'); - if (isDateTime) { - self.xvaluesAreIndex = true; - x = index; + var _date = moment(x); + if (_date.isValid()) { + x = _date.toDate().getTime(); + } } else if (typeof x === 'string') { x = parseFloat(x); if (isNaN(x)) { // assume this is a string label diff --git a/test/index.html b/test/index.html index f51ccd39..9cf784c2 100644 --- a/test/index.html +++ b/test/index.html @@ -5,6 +5,7 @@ Qunit Tests + @@ -14,6 +15,8 @@ + + diff --git a/test/view.flot.test.js b/test/view.flot.test.js index 005250cd..fb458181 100644 --- a/test/view.flot.test.js +++ b/test/view.flot.test.js @@ -48,7 +48,9 @@ test('dates in graph view', function () { 'series': ['y', 'z'] } }); + view.render(); $('.fixtures').append(view.el); + view.redraw(); view.remove(); }); diff --git a/vendor/flot/jquery.flot.js b/vendor/flot/jquery.flot.js index 8cfa6113..aa7e362a 100644 --- a/vendor/flot/jquery.flot.js +++ b/vendor/flot/jquery.flot.js @@ -1,8 +1,9 @@ -/*! Javascript plotting library for jQuery, version 0.8 alpha. - * - * Released under the MIT license by IOLA, December 2007. - * - */ +/* Javascript plotting library for jQuery, version 0.8.1. + +Copyright (c) 2007-2013 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ // first an inline dependency, jquery.colorhelpers.js, we inline it here // for convenience @@ -32,6 +33,462 @@ // the actual Flot code (function($) { + + // Cache the prototype hasOwnProperty for faster access + + var hasOwnProperty = Object.prototype.hasOwnProperty; + + /////////////////////////////////////////////////////////////////////////// + // The Canvas object is a wrapper around an HTML5 tag. + // + // @constructor + // @param {string} cls List of classes to apply to the canvas. + // @param {element} container Element onto which to append the canvas. + // + // Requiring a container is a little iffy, but unfortunately canvas + // operations don't work unless the canvas is attached to the DOM. + + function Canvas(cls, container) { + + var element = container.children("." + cls)[0]; + + if (element == null) { + + element = document.createElement("canvas"); + element.className = cls; + + $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) + .appendTo(container); + + // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas + + if (!element.getContext) { + if (window.G_vmlCanvasManager) { + element = window.G_vmlCanvasManager.initElement(element); + } else { + throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); + } + } + } + + this.element = element; + + var context = this.context = element.getContext("2d"); + + // Determine the screen's ratio of physical to device-independent + // pixels. This is the ratio between the canvas width that the browser + // advertises and the number of pixels actually present in that space. + + // The iPhone 4, for example, has a device-independent width of 320px, + // but its screen is actually 640px wide. It therefore has a pixel + // ratio of 2, while most normal devices have a ratio of 1. + + var devicePixelRatio = window.devicePixelRatio || 1, + backingStoreRatio = + context.webkitBackingStorePixelRatio || + context.mozBackingStorePixelRatio || + context.msBackingStorePixelRatio || + context.oBackingStorePixelRatio || + context.backingStorePixelRatio || 1; + + this.pixelRatio = devicePixelRatio / backingStoreRatio; + + // Size the canvas to match the internal dimensions of its container + + this.resize(container.width(), container.height()); + + // Collection of HTML div layers for text overlaid onto the canvas + + this.textContainer = null; + this.text = {}; + + // Cache of text fragments and metrics, so we can avoid expensively + // re-calculating them when the plot is re-rendered in a loop. + + this._textCache = {}; + } + + // Resizes the canvas to the given dimensions. + // + // @param {number} width New width of the canvas, in pixels. + // @param {number} width New height of the canvas, in pixels. + + Canvas.prototype.resize = function(width, height) { + + if (width <= 0 || height <= 0) { + throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); + } + + var element = this.element, + context = this.context, + pixelRatio = this.pixelRatio; + + // Resize the canvas, increasing its density based on the display's + // pixel ratio; basically giving it more pixels without increasing the + // size of its element, to take advantage of the fact that retina + // displays have that many more pixels in the same advertised space. + + // Resizing should reset the state (excanvas seems to be buggy though) + + if (this.width != width) { + element.width = width * pixelRatio; + element.style.width = width + "px"; + this.width = width; + } + + if (this.height != height) { + element.height = height * pixelRatio; + element.style.height = height + "px"; + this.height = height; + } + + // Save the context, so we can reset in case we get replotted. The + // restore ensure that we're really back at the initial state, and + // should be safe even if we haven't saved the initial state yet. + + context.restore(); + context.save(); + + // Scale the coordinate space to match the display density; so even though we + // may have twice as many pixels, we still want lines and other drawing to + // appear at the same size; the extra pixels will just make them crisper. + + context.scale(pixelRatio, pixelRatio); + }; + + // Clears the entire canvas area, not including any overlaid HTML text + + Canvas.prototype.clear = function() { + this.context.clearRect(0, 0, this.width, this.height); + }; + + // Finishes rendering the canvas, including managing the text overlay. + + Canvas.prototype.render = function() { + + var cache = this._textCache; + + // For each text layer, add elements marked as active that haven't + // already been rendered, and remove those that are no longer active. + + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { + + var layer = this.getTextLayer(layerKey), + layerCache = cache[layerKey]; + + layer.hide(); + + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + + var positions = styleCache[key].positions; + + for (var i = 0, position; position = positions[i]; i++) { + if (position.active) { + if (!position.rendered) { + layer.append(position.element); + position.rendered = true; + } + } else { + positions.splice(i--, 1); + if (position.rendered) { + position.element.detach(); + } + } + } + + if (positions.length == 0) { + delete styleCache[key]; + } + } + } + } + } + + layer.show(); + } + } + }; + + // Creates (if necessary) and returns the text overlay container. + // + // @param {string} classes String of space-separated CSS classes used to + // uniquely identify the text layer. + // @return {object} The jQuery-wrapped text-layer div. + + Canvas.prototype.getTextLayer = function(classes) { + + var layer = this.text[classes]; + + // Create the text layer if it doesn't exist + + if (layer == null) { + + // Create the text layer container, if it doesn't exist + + if (this.textContainer == null) { + this.textContainer = $("
") + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, + 'font-size': "smaller", + color: "#545454" + }) + .insertAfter(this.element); + } + + layer = this.text[classes] = $("
") + .addClass(classes) + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + }) + .appendTo(this.textContainer); + } + + return layer; + }; + + // Creates (if necessary) and returns a text info object. + // + // The object looks like this: + // + // { + // width: Width of the text's wrapper div. + // height: Height of the text's wrapper div. + // element: The jQuery-wrapped HTML div containing the text. + // positions: Array of positions at which this text is drawn. + // } + // + // The positions array contains objects that look like this: + // + // { + // active: Flag indicating whether the text should be visible. + // rendered: Flag indicating whether the text is currently visible. + // element: The jQuery-wrapped HTML div containing the text. + // x: X coordinate at which to draw the text. + // y: Y coordinate at which to draw the text. + // } + // + // Each position after the first receives a clone of the original element. + // + // The idea is that that the width, height, and general 'identity' of the + // text is constant no matter where it is placed; the placements are a + // secondary property. + // + // Canvas maintains a cache of recently-used text info objects; getTextInfo + // either returns the cached element or creates a new entry. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {string} text Text string to retrieve info for. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @return {object} a text info object. + + Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { + + var textStyle, layerCache, styleCache, info; + + // Cast the value to a string, in case we were given a number or such + + text = "" + text; + + // If the font is a font-spec object, generate a CSS font definition + + if (typeof font === "object") { + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; + } else { + textStyle = font; + } + + // Retrieve (or create) the cache for the text's layer and styles + + layerCache = this._textCache[layer]; + + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; + } + + styleCache = layerCache[textStyle]; + + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } + + info = styleCache[text]; + + // If we can't find a matching element in our cache, create a new one + + if (info == null) { + + var element = $("
").html(text) + .css({ + position: "absolute", + 'max-width': width, + top: -9999 + }) + .appendTo(this.getTextLayer(layer)); + + if (typeof font === "object") { + element.css({ + font: textStyle, + color: font.color + }); + } else if (typeof font === "string") { + element.addClass(font); + } + + info = styleCache[text] = { + width: element.outerWidth(true), + height: element.outerHeight(true), + element: element, + positions: [] + }; + + element.detach(); + } + + return info; + }; + + // Adds a text string to the canvas text overlay. + // + // The text isn't drawn immediately; it is marked as rendering, which will + // result in its addition to the canvas on the next render pass. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number} x X coordinate at which to draw the text. + // @param {number} y Y coordinate at which to draw the text. + // @param {string} text Text string to draw. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @param {string=} halign Horizontal alignment of the text; either "left", + // "center" or "right". + // @param {string=} valign Vertical alignment of the text; either "top", + // "middle" or "bottom". + + Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { + + var info = this.getTextInfo(layer, text, font, angle, width), + positions = info.positions; + + // Tweak the div's position to match the text's alignment + + if (halign == "center") { + x -= info.width / 2; + } else if (halign == "right") { + x -= info.width; + } + + if (valign == "middle") { + y -= info.height / 2; + } else if (valign == "bottom") { + y -= info.height; + } + + // Determine whether this text already exists at this position. + // If so, mark it for inclusion in the next render pass. + + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = true; + return; + } + } + + // If the text doesn't exist at this position, create a new entry + + // For the very first position we'll re-use the original element, + // while for subsequent ones we'll clone it. + + position = { + active: true, + rendered: false, + element: positions.length ? info.element.clone() : info.element, + x: x, + y: y + } + + positions.push(position); + + // Move the element to its final position within the container + + position.element.css({ + top: Math.round(y), + left: Math.round(x), + 'text-align': halign // In case the text wraps + }); + }; + + // Removes one or more text strings from the canvas text overlay. + // + // If no parameters are given, all text within the layer is removed. + // + // Note that the text is not immediately removed; it is simply marked as + // inactive, which will result in its removal on the next render pass. + // This avoids the performance penalty for 'clear and redraw' behavior, + // where we potentially get rid of all text on a layer, but will likely + // add back most or all of it later, as when redrawing axes, for example. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number=} x X coordinate of the text. + // @param {number=} y Y coordinate of the text. + // @param {string=} text Text string to remove. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which the text is rotated, in degrees. + // Angle is currently unused, it will be implemented in the future. + + Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { + if (text == null) { + var layerCache = this._textCache[layer]; + if (layerCache != null) { + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + var positions = styleCache[key].positions; + for (var i = 0, position; position = positions[i]; i++) { + position.active = false; + } + } + } + } + } + } + } else { + var positions = this.getTextInfo(layer, text, font, angle).positions; + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = false; + } + } + } + }; + + /////////////////////////////////////////////////////////////////////////// + // The top-level container for the entire plot. + function Plot(placeholder, data_, options_, plugins) { // data is on the form: // [ series1, series2 ... ] @@ -58,8 +515,7 @@ show: null, // null = auto-detect, true = always, false = never position: "bottom", // or "top" mode: null, // null or "time" - timezone: null, // "browser" for local to the client or timezone for timezone-js - font: null, // null (derived from CSS in placeholder) or object like { size: 11, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } + font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } 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 @@ -74,14 +530,9 @@ 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 tickSize: null, // number or [number, "unit"] - minTickSize: null, // number or [number, "unit"] - monthNames: null, // list of names of months - timeformat: null, // format string to use - twelveHourClock: false // 12 or 24 time in time mode + minTickSize: null // number or [number, "unit"] }, yaxis: { autoscaleMargin: 0.02, @@ -105,6 +556,8 @@ fill: false, fillColor: null, steps: false + // Omit 'zero', so we can later default its value to + // match that of the 'fill' option. }, bars: { show: false, @@ -113,7 +566,8 @@ fill: true, fillColor: null, align: "left", // "left", "right", or "center" - horizontal: false + horizontal: false, + zero: true }, shadowSize: 3, highlightColor: null @@ -144,13 +598,12 @@ }, hooks: {} }, - canvas = null, // the canvas for the plot itself + surface = null, // the canvas for the plot itself overlay = null, // canvas for interactive stuff on top of plot eventHolder = null, // jQuery object that events should be bound to ctx = null, octx = null, xaxes = [], yaxes = [], plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, - canvasWidth = 0, canvasHeight = 0, plotWidth = 0, plotHeight = 0, hooks = { processOptions: [], @@ -171,7 +624,7 @@ plot.setupGrid = setupGrid; plot.draw = draw; plot.getPlaceholder = function() { return placeholder; }; - plot.getCanvas = function() { return canvas; }; + plot.getCanvas = function() { return surface.element; }; plot.getPlotOffset = function() { return plotOffset; }; plot.width = function () { return plotWidth; }; plot.height = function () { return plotHeight; }; @@ -206,9 +659,10 @@ }; plot.shutdown = shutdown; plot.resize = function () { - getCanvasDimensions(); - resizeCanvas(canvas); - resizeCanvas(overlay); + var width = placeholder.width(), + height = placeholder.height(); + surface.resize(width, height); + overlay.resize(width, height); }; // public attributes @@ -231,40 +685,103 @@ } function initPlugins() { + + // References to key classes, allowing plugins to modify them + + var classes = { + Canvas: Canvas + }; + for (var i = 0; i < plugins.length; ++i) { var p = plugins[i]; - p.init(plot); + p.init(plot, classes); if (p.options) $.extend(true, options, p.options); } } 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; + // $.extend merges arrays, rather than replacing them. When less + // colors are provided than the size of the default palette, we + // end up with those colors plus the remaining defaults, which is + // not expected behavior; avoid it by replacing them here. - 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 (opts && opts.colors) { + options.colors = opts.colors; + } + + if (options.xaxis.color == null) + options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + if (options.yaxis.color == null) + options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility + options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; + if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility + options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; 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]); + // Fill in defaults for axis options, including any unspecified + // font-spec fields, if a font-spec was provided. + + // If no x/y axis options were provided, create one of each anyway, + // since the rest of the code assumes that they exist. + + var i, axisOptions, axisCount, + fontDefaults = { + style: placeholder.css("font-style"), + size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)), + variant: placeholder.css("font-variant"), + weight: placeholder.css("font-weight"), + family: placeholder.css("font-family") + }; + + fontDefaults.lineHeight = fontDefaults.size * 1.15; + + axisCount = options.xaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.xaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.xaxis, axisOptions); + options.xaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + } + } + + axisCount = options.yaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.yaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.yaxis, axisOptions); + options.yaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + } + } // backwards compatibility, to be removed in future if (options.xaxis.noTicks && options.xaxis.ticks == null) @@ -420,10 +937,10 @@ function fillInSeriesOptions() { - var neededColors = series.length, maxIndex = 0, i; + var neededColors = series.length, maxIndex = -1, i; - // Subtract the number of series that already have fixed - // colors from the number we need to generate. + // Subtract the number of series that already have fixed colors or + // color indexes from the number that we still need to generate. for (i = 0; i < series.length; ++i) { var sc = series[i].color; @@ -435,14 +952,15 @@ } } - // If any of the user colors are numeric indexes, then we - // need to generate at least as many as the highest index. + // If any of the series have fixed color indexes, then we need to + // generate at least as many colors as the highest index. - if (maxIndex > neededColors) { + if (neededColors <= maxIndex) { neededColors = maxIndex + 1; } - // Generate the needed colors, based on the option colors + // Generate all the colors, using first the option colors and then + // variations on those colors once they're exhausted. var c, colors = [], colorPool = options.colors, colorPoolSize = colorPool.length, variation = 0; @@ -496,6 +1014,13 @@ s.lines.show = true; } + // If nothing was provided for lines.zero, default it to match + // lines.fill, since areas by default should extend to zero. + + if (s.lines.zero == null) { + s.lines.zero = !!s.lines.fill; + } + // setup axes s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); @@ -545,7 +1070,8 @@ format.push({ y: true, number: true, required: true }); if (s.bars.show || (s.lines.show && s.lines.fill)) { - format.push({ y: true, number: true, required: false, defaultValue: 0 }); + var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); + format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); if (s.bars.horizontal) { delete format[format.length - 1].y; format[format.length - 1].x = true; @@ -605,10 +1131,14 @@ if (val != null) { f = format[m]; // extract min/max info - if (f.x) - updateAxis(s.xaxis, val, val); - if (f.y) - updateAxis(s.yaxis, val, val); + if (f.autoscale) { + if (f.x) { + updateAxis(s.xaxis, val, val); + } + if (f.y) { + updateAxis(s.yaxis, val, val); + } + } } points[k + m] = null; } @@ -645,7 +1175,7 @@ // second pass: find datamax/datamin for auto-scaling for (i = 0; i < series.length; ++i) { s = series[i]; - points = s.datapoints.points, + points = s.datapoints.points; ps = s.datapoints.pointsize; format = s.datapoints.format; @@ -659,7 +1189,7 @@ for (m = 0; m < ps; ++m) { val = points[j + m]; f = format[m]; - if (!f || val == fakeInfinity || val == -fakeInfinity) + if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) continue; if (f.x) { @@ -717,163 +1247,33 @@ }); } - ////////////////////////////////////////////////////////////////////////////////// - // Returns the display's ratio between physical and device-independent pixels. - // - // This is the ratio between the width that the browser advertises and the number - // of pixels actually available in that space. The iPhone 4, for example, has a - // device-independent width of 320px, but its screen is actually 640px wide. It - // therefore has a pixel ratio of 2, while most normal devices have a ratio of 1. - - function getPixelRatio(cctx) { - var devicePixelRatio = window.devicePixelRatio || 1; - var backingStoreRatio = - cctx.webkitBackingStorePixelRatio || - cctx.mozBackingStorePixelRatio || - cctx.msBackingStorePixelRatio || - cctx.oBackingStorePixelRatio || - cctx.backingStorePixelRatio || 1; - - return devicePixelRatio / backingStoreRatio; - } - - function makeCanvas(skipPositioning, cls) { - - var c = document.createElement('canvas'); - c.className = cls; - - if (!skipPositioning) - $(c).css({ position: 'absolute', left: 0, top: 0 }); - - $(c).appendTo(placeholder); - - // If HTML5 Canvas isn't available, fall back to Excanvas - - if (!c.getContext) { - if (window.G_vmlCanvasManager) { - c = window.G_vmlCanvasManager.initElement(c); - } else { - throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); - } - } - - var cctx = c.getContext("2d"); - - // Increase the canvas density based on the display's pixel ratio; basically - // giving the canvas more pixels without increasing the size of its element, - // to take advantage of the fact that retina displays have that many more - // pixels than they actually use for page & element widths. - - var pixelRatio = getPixelRatio(cctx); - - c.width = canvasWidth * pixelRatio; - c.height = canvasHeight * pixelRatio; - c.style.width = canvasWidth + "px"; - c.style.height = canvasHeight + "px"; - - // Save the context so we can reset in case we get replotted - - cctx.save(); - - // Scale the coordinate space to match the display density; so even though we - // may have twice as many pixels, we still want lines and other drawing to - // appear at the same size; the extra pixels will just make them crisper. - - cctx.scale(pixelRatio, pixelRatio); - - return c; - } - - function getCanvasDimensions() { - canvasWidth = placeholder.width(); - canvasHeight = placeholder.height(); - - if (canvasWidth <= 0 || canvasHeight <= 0) - throw new Error("Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight); - } - - function resizeCanvas(c) { - - var cctx = c.getContext("2d"); - - // Handle pixel ratios > 1 for retina displays, as explained in makeCanvas - - var pixelRatio = getPixelRatio(cctx); - - // Resizing should reset the state (excanvas seems to be buggy though) - - if (c.style.width != canvasWidth) { - c.width = canvasWidth * pixelRatio; - c.style.width = canvasWidth + "px"; - } - - if (c.style.height != canvasHeight) { - c.height = canvasHeight * pixelRatio; - c.style.height = canvasHeight + "px"; - } - - // so try to get back to the initial state (even if it's - // gone now, this should be safe according to the spec) - cctx.restore(); - - // and save again - cctx.save(); - - // Apply scaling for retina displays, as explained in makeCanvas - - cctx.scale(pixelRatio, pixelRatio); - } - function setupCanvases() { - var reused, - existingCanvas = placeholder.children("canvas.flot-base"), - existingOverlay = placeholder.children("canvas.flot-overlay"); - if (existingCanvas.length == 0 || existingOverlay == 0) { - // init everything + // Make sure the placeholder is clear of everything except canvases + // from a previous plot in this container that we'll try to re-use. - placeholder.html(""); // make sure placeholder is clear + placeholder.css("padding", 0) // padding messes up the positioning + .children(":not(.flot-base,.flot-overlay)").remove(); - placeholder.css({ padding: 0 }); // padding messes up the positioning + if (placeholder.css("position") == 'static') + placeholder.css("position", "relative"); // for positioning labels and overlay - if (placeholder.css("position") == 'static') - placeholder.css("position", "relative"); // for positioning labels and overlay + surface = new Canvas("flot-base", placeholder); + overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features - getCanvasDimensions(); - - canvas = makeCanvas(true, "flot-base"); - overlay = makeCanvas(false, "flot-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"); + ctx = surface.context; + octx = overlay.context; // define which element we're listening for events on - eventHolder = $(overlay); + eventHolder = $(overlay.element).unbind(); - if (reused) { - // run shutdown in the old plot object - placeholder.data("plot").shutdown(); + // If we're re-using a plot object, shut down the old one - // reset reused canvases - plot.resize(); + var existing = placeholder.data("plot"); - // 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(); + if (existing) { + existing.shutdown(); + overlay.clear(); } // save in case we get replotted @@ -884,7 +1284,14 @@ // bind events if (options.grid.hoverable) { eventHolder.mousemove(onMouseMove); - eventHolder.mouseleave(onMouseLeave); + + // Use bind, rather than .mouseleave, because we officially + // still support jQuery 1.2.6, which doesn't define a shortcut + // for mouseenter or mouseleave. This was a bug/oversight that + // was fixed somewhere around 1.3.x. We can return to using + // .mouseleave when we drop support for 1.2.6. + + eventHolder.bind("mouseleave", onMouseLeave); } if (options.grid.clickable) @@ -938,56 +1345,31 @@ } function measureTickLabels(axis) { - var opts = axis.options, ticks = axis.ticks || [], - axisw = opts.labelWidth || 0, axish = opts.labelHeight || 0, - f = axis.font; - ctx.save(); - ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px '" + f.family + "'"; + var opts = axis.options, + ticks = axis.ticks || [], + labelWidth = opts.labelWidth || 0, + labelHeight = opts.labelHeight || 0, + maxWidth = labelWidth || axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null; + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = opts.font || "flot-tick-label tickLabel"; for (var i = 0; i < ticks.length; ++i) { - var t = ticks[i]; - t.lines = []; - t.width = t.height = 0; + var t = ticks[i]; if (!t.label) continue; - // accept various kinds of newlines, including HTML ones - // (you can actually split directly on regexps in Javascript, - // but IE is unfortunately broken) - var lines = (t.label + "").replace(/
|\r\n|\r/g, "\n").split("\n"); - for (var j = 0; j < lines.length; ++j) { - var line = { text: lines[j] }, - m = ctx.measureText(line.text); + var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); - line.width = m.width; - // m.height might not be defined, not in the - // standard yet - line.height = m.height != null ? m.height : f.size; - - // add a bit of margin since font rendering is - // not pixel perfect and cut off letters look - // bad, this also doubles as spacing between - // lines - line.height += Math.round(f.size * 0.15); - - t.width = Math.max(line.width, t.width); - t.height += line.height; - - t.lines.push(line); - } - - if (opts.labelWidth == null) - axisw = Math.max(axisw, t.width); - if (opts.labelHeight == null) - axish = Math.max(axish, t.height); + labelWidth = Math.max(labelWidth, info.width); + labelHeight = Math.max(labelHeight, info.height); } - ctx.restore(); - axis.labelWidth = Math.ceil(axisw); - axis.labelHeight = Math.ceil(axish); + axis.labelWidth = opts.labelWidth || labelWidth; + axis.labelHeight = opts.labelHeight || labelHeight; } function allocateAxisBoxFirstPhase(axis) { @@ -1035,7 +1417,7 @@ if (pos == "bottom") { plotOffset.bottom += lh + axisMargin; - axis.box = { top: canvasHeight - plotOffset.bottom, height: lh }; + axis.box = { top: surface.height - plotOffset.bottom, height: lh }; } else { axis.box = { top: plotOffset.top + axisMargin, height: lh }; @@ -1051,7 +1433,7 @@ } else { plotOffset.right += lw + axisMargin; - axis.box = { left: canvasWidth - plotOffset.right, width: lw }; + axis.box = { left: surface.width - plotOffset.right, width: lw }; } } @@ -1067,11 +1449,11 @@ // dimension, we can set the remaining dimension coordinates if (axis.direction == "x") { axis.box.left = plotOffset.left - axis.labelWidth / 2; - axis.box.width = canvasWidth - plotOffset.left - plotOffset.right + axis.labelWidth; + axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; } else { axis.box.top = plotOffset.top - axis.labelHeight / 2; - axis.box.height = canvasHeight - plotOffset.bottom - plotOffset.top + axis.labelHeight; + axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; } } @@ -1124,10 +1506,10 @@ for (var a in plotOffset) { if(typeof(options.grid.borderWidth) == "object") { - plotOffset[a] = showGrid ? options.grid.borderWidth[a] : 0; + plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; } else { - plotOffset[a] = showGrid ? options.grid.borderWidth : 0; + plotOffset[a] += showGrid ? options.grid.borderWidth : 0; } } @@ -1143,14 +1525,6 @@ }); if (showGrid) { - // determine from the placeholder the font size ~ height of font ~ 1 em - var fontDefaults = { - style: placeholder.css("font-style"), - size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)), - variant: placeholder.css("font-variant"), - weight: placeholder.css("font-weight"), - family: placeholder.css("font-family") - }; var allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; }); @@ -1159,9 +1533,7 @@ setupTickGeneration(axis); setTicks(axis); snapRangeToTicks(axis, axis.ticks); - // find labelWidth/Height for axis - axis.font = $.extend({}, fontDefaults, axis.options.font); measureTickLabels(axis); }); @@ -1180,14 +1552,18 @@ }); } - plotWidth = canvasWidth - plotOffset.left - plotOffset.right; - plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top; + plotWidth = surface.width - plotOffset.left - plotOffset.right; + plotHeight = surface.height - plotOffset.bottom - plotOffset.top; // now we got the proper plot dimensions, we can compute the scaling $.each(axes, function (_, axis) { setTransformationHelpers(axis); }); + if (showGrid) { + drawAxisLabels(); + } + insertLegend(); } @@ -1240,9 +1616,44 @@ else // 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); + noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); - axis.delta = (axis.max - axis.min) / noTicks; + var delta = (axis.max - axis.min) / noTicks, + dec = -Math.floor(Math.log(delta) / Math.LN10), + maxDec = opts.tickDecimals; + + if (maxDec != null && dec > maxDec) { + dec = maxDec; + } + + var magn = Math.pow(10, -dec), + norm = delta / magn, // norm is between 1.0 and 10.0 + size; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + // special case for 2.5, requires an extra decimal + if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + + if (opts.minTickSize != null && size < opts.minTickSize) { + size = opts.minTickSize; + } + + axis.delta = delta; + axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); + axis.tickSize = opts.tickSize || size; // Time mode was moved to a plug-in in 0.8, but since so many people use this // we'll add an especially friendly make sure they remembered to include it. @@ -1256,40 +1667,14 @@ if (!axis.tickGenerator) { - var maxDec = opts.tickDecimals; - var dec = -Math.floor(Math.log(axis.delta) / Math.LN10); - if (maxDec != null && dec > maxDec) - dec = maxDec; - - var magn = Math.pow(10, -dec); - var norm = axis.delta / magn; // norm is between 1.0 and 10.0 - var size; - - if (norm < 1.5) - size = 1; - else if (norm < 3) { - size = 2; - // special case for 2.5, requires an extra decimal - if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { - size = 2.5; - ++dec; - } - } - else if (norm < 7.5) - size = 5; - else size = 10; - - size *= magn; - - if (opts.minTickSize != null && size < opts.minTickSize) - size = opts.minTickSize; - - axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); - axis.tickSize = opts.tickSize || size; - axis.tickGenerator = function (axis) { - var ticks = [], start = floorInBase(axis.min, axis.tickSize), - i = 0, v = Number.NaN, prev; + + var ticks = [], + start = floorInBase(axis.min, axis.tickSize), + i = 0, + v = Number.NaN, + prev; + do { prev = v; v = start + i * axis.tickSize; @@ -1301,7 +1686,7 @@ axis.tickFormatter = function (value, axis) { - var factor = Math.pow(10, axis.tickDecimals); + var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; var formatted = "" + Math.round(value * factor) / factor; // If tickDecimals was specified, ensure that we have exactly that @@ -1404,7 +1789,8 @@ } function draw() { - ctx.clearRect(0, 0, canvasWidth, canvasHeight); + + surface.clear(); executeHooks(hooks.drawBackground, [ctx]); @@ -1416,7 +1802,6 @@ if (grid.show && !grid.aboveData) { drawGrid(); - drawAxisLabels(); } for (var i = 0; i < series.length; ++i) { @@ -1428,8 +1813,14 @@ if (grid.show && grid.aboveData) { drawGrid(); - drawAxisLabels(); } + + surface.render(); + + // A draw implies that either the axes or data have changed, so we + // should probably update the overlay highlights as well. + + triggerRedrawOverlay(); } function extractRange(ranges, coord) { @@ -1559,7 +1950,6 @@ 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 @@ -1580,16 +1970,20 @@ // draw tick bar if (!axis.innermost) { + ctx.strokeStyle = axis.options.color; ctx.beginPath(); xoff = yoff = 0; if (axis.direction == "x") - xoff = plotWidth; + xoff = plotWidth + 1; else - yoff = plotHeight; + yoff = plotHeight + 1; if (ctx.lineWidth == 1) { - x = Math.floor(x) + 0.5; - y = Math.floor(y) + 0.5; + if (axis.direction == "x") { + y = Math.floor(y) + 0.5; + } else { + x = Math.floor(x) + 0.5; + } } ctx.moveTo(x, y); @@ -1598,13 +1992,16 @@ } // draw ticks + + ctx.strokeStyle = axis.options.tickColor; + ctx.beginPath(); for (i = 0; i < axis.ticks.length; ++i) { var v = axis.ticks[i].v; xoff = yoff = 0; - if (v < axis.min || v > axis.max + if (isNaN(v) || v < axis.min || v > axis.max // skip those lying on the axes if we got a border || (t == "full" && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) @@ -1701,72 +2098,48 @@ } function drawAxisLabels() { - ctx.save(); $.each(allAxes(), function (_, axis) { if (!axis.show || axis.ticks.length == 0) return; - var box = axis.box, f = axis.font; - // placeholder.append('
') // debug + var box = axis.box, + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = axis.options.font || "flot-tick-label tickLabel", + tick, x, y, halign, valign; - ctx.fillStyle = axis.options.color; - // Important: Don't use quotes around axis.font.family! Just around single - // font names like 'Times New Roman' that have a space or special character in it. - ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px " + f.family; - ctx.textAlign = "start"; - // middle align the labels - top would be more - // natural, but browsers can differ a pixel or two in - // where they consider the top to be, so instead we - // middle align to minimize variation between browsers - // and compensate when calculating the coordinates - ctx.textBaseline = "middle"; + surface.removeText(layer); for (var i = 0; i < axis.ticks.length; ++i) { - var tick = axis.ticks[i]; + + tick = axis.ticks[i]; if (!tick.label || tick.v < axis.min || tick.v > axis.max) continue; - var x, y, offset = 0, line; - for (var k = 0; k < tick.lines.length; ++k) { - line = tick.lines[k]; - - if (axis.direction == "x") { - x = plotOffset.left + axis.p2c(tick.v) - line.width/2; - if (axis.position == "bottom") - y = box.top + box.padding; - else - y = box.top + box.height - box.padding - tick.height; + if (axis.direction == "x") { + halign = "center"; + x = plotOffset.left + axis.p2c(tick.v); + if (axis.position == "bottom") { + y = box.top + box.padding; + } else { + y = box.top + box.height - box.padding; + valign = "bottom"; } - else { - y = plotOffset.top + axis.p2c(tick.v) - tick.height/2; - if (axis.position == "left") - x = box.left + box.width - box.padding - line.width; - else - x = box.left + box.padding; + } else { + valign = "middle"; + y = plotOffset.top + axis.p2c(tick.v); + if (axis.position == "left") { + x = box.left + box.width - box.padding; + halign = "right"; + } else { + x = box.left + box.padding; } - - // account for middle aligning and line number - y += line.height/2 + offset; - offset += line.height; - - if ($.browser.opera) { - // FIXME: UGLY BROWSER DETECTION - // round the coordinates since Opera - // otherwise switches to more ugly - // rendering (probably non-hinted) and - // offset the y coordinates since it seems - // to be off pretty consistently compared - // to the other browsers - x = Math.floor(x); - y = Math.ceil(y - 2); - } - ctx.fillText(line.text, x, y); } + + surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); } }); - - ctx.restore(); } function drawSeries(series) { @@ -2065,6 +2438,15 @@ sw = series.shadowSize, radius = series.points.radius, symbol = series.points.symbol; + + // If the user sets the line width to 0, we change it to a very + // small value. A line width of 0 seems to force the default of 1. + // Doing the conditional here allows the shadow setting to still be + // optional even with a lineWidth of 0. + + if( lw == 0 ) + lw = 0.0001; + if (lw > 0 && sw > 0) { // draw shadow in two steps var w = sw / 2; @@ -2279,6 +2661,8 @@ if (options.legend.sorted) { if ($.isFunction(options.legend.sorted)) { entries.sort(options.legend.sorted); + } else if (options.legend.sorted == "reverse") { + entries.reverse(); } else { var ascending = options.legend.sorted != "descending"; entries.sort(function(a, b) { @@ -2293,7 +2677,7 @@ for (var i = 0; i < entries.length; ++i) { - entry = entries[i]; + var entry = entries[i]; if (i % options.legend.noColumns == 0) { if (rowStarted) @@ -2449,7 +2833,7 @@ function onMouseMove(e) { if (options.grid.hoverable) triggerClickHoverEvent("plothover", e, - function (s) { return !!s["hoverable"]; }); + function (s) { return s["hoverable"] != false; }); } function onMouseLeave(e) { @@ -2460,7 +2844,7 @@ function onClick(e) { triggerClickHoverEvent("plotclick", e, - function (s) { return !!s["clickable"]; }); + function (s) { return s["clickable"] != false; }); } // trigger click or hover event (they send the same parameters @@ -2516,7 +2900,7 @@ // draw highlights octx.save(); - octx.clearRect(0, 0, canvasWidth, canvasHeight); + overlay.clear(); octx.translate(plotOffset.left, plotOffset.top); var i, hi; @@ -2556,13 +2940,16 @@ if (s == null && point == null) { highlights = []; triggerRedrawOverlay(); + return; } 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) { @@ -2584,7 +2971,7 @@ function drawPointHighlight(series, point) { var x = point[0], y = point[1], - axisx = series.xaxis, axisy = series.yaxis; + axisx = series.xaxis, axisy = series.yaxis, highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) @@ -2645,6 +3032,8 @@ } } + // Add the plot function to the top level of the jQuery object + $.plot = function(placeholder, data, options) { //var t0 = new Date(); var plot = new Plot($(placeholder), data, options, $.plot.plugins); @@ -2652,10 +3041,18 @@ return plot; }; - $.plot.version = "0.7"; + $.plot.version = "0.8.1"; $.plot.plugins = []; + // Also add the plot function as a chainable property + + $.fn.plot = function(data, options) { + return this.each(function() { + $.plot(this, data, options); + }); + }; + // round to nearby lower multiple of base function floorInBase(n, base) { return base * Math.floor(n / base); diff --git a/vendor/flot/jquery.flot.time.js b/vendor/flot/jquery.flot.time.js new file mode 100644 index 00000000..15f52815 --- /dev/null +++ b/vendor/flot/jquery.flot.time.js @@ -0,0 +1,431 @@ +/* Pretty handling of time axes. + +Copyright (c) 2007-2013 IOLA and Ole Laursen. +Licensed under the MIT license. + +Set axis.mode to "time" to enable. See the section "Time series data" in +API.txt for details. + +*/ + +(function($) { + + var options = { + xaxis: { + timezone: null, // "browser" for local to the client or timezone for timezone-js + timeformat: null, // format string to use + twelveHourClock: false, // 12 or 24 time in time mode + monthNames: null // list of names of months + } + }; + + // round to nearby lower multiple of base + + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + + // Returns a string with the date d formatted according to fmt. + // A subset of the Open Group's strftime format is supported. + + function formatDate(d, fmt, monthNames, dayNames) { + + if (typeof d.strftime == "function") { + return d.strftime(fmt); + } + + var leftPad = function(n, pad) { + n = "" + n; + pad = "" + (pad == null ? "0" : pad); + return n.length == 1 ? pad + n : n; + }; + + var r = []; + var escape = false; + var hours = d.getHours(); + var isAM = hours < 12; + + if (monthNames == null) { + monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + } + + if (dayNames == null) { + dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + } + + var hours12; + + if (hours > 12) { + hours12 = hours - 12; + } else if (hours == 0) { + hours12 = 12; + } else { + hours12 = hours; + } + + for (var i = 0; i < fmt.length; ++i) { + + var c = fmt.charAt(i); + + if (escape) { + switch (c) { + case 'a': c = "" + dayNames[d.getDay()]; break; + case 'b': c = "" + monthNames[d.getMonth()]; break; + case 'd': c = leftPad(d.getDate()); break; + case 'e': c = leftPad(d.getDate(), " "); break; + case 'h': // For back-compat with 0.7; remove in 1.0 + case 'H': c = leftPad(hours); break; + case 'I': c = leftPad(hours12); break; + case 'l': c = leftPad(hours12, " "); break; + case 'm': c = leftPad(d.getMonth() + 1); break; + case 'M': c = leftPad(d.getMinutes()); break; + // quarters not in Open Group's strftime specification + case 'q': + c = "" + (Math.floor(d.getMonth() / 3) + 1); break; + case 'S': c = leftPad(d.getSeconds()); break; + case 'y': c = leftPad(d.getFullYear() % 100); break; + case 'Y': c = "" + d.getFullYear(); break; + case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; + case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; + case 'w': c = "" + d.getDay(); break; + } + r.push(c); + escape = false; + } else { + if (c == "%") { + escape = true; + } else { + r.push(c); + } + } + } + + return r.join(""); + } + + // To have a consistent view of time-based data independent of which time + // zone the client happens to be in we need a date-like object independent + // of time zones. This is done through a wrapper that only calls the UTC + // versions of the accessor methods. + + function makeUtcWrapper(d) { + + function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { + sourceObj[sourceMethod] = function() { + return targetObj[targetMethod].apply(targetObj, arguments); + }; + }; + + var utc = { + date: d + }; + + // support strftime, if found + + if (d.strftime != undefined) { + addProxyMethod(utc, "strftime", d, "strftime"); + } + + addProxyMethod(utc, "getTime", d, "getTime"); + addProxyMethod(utc, "setTime", d, "setTime"); + + var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; + + for (var p = 0; p < props.length; p++) { + addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); + addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); + } + + return utc; + }; + + // select time zone strategy. This returns a date-like object tied to the + // desired timezone + + function dateGenerator(ts, opts) { + if (opts.timezone == "browser") { + return new Date(ts); + } else if (!opts.timezone || opts.timezone == "utc") { + return makeUtcWrapper(new Date(ts)); + } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { + var d = new timezoneJS.Date(); + // timezone-js is fickle, so be sure to set the time zone before + // setting the time. + d.setTimezone(opts.timezone); + d.setTime(ts); + return d; + } else { + return makeUtcWrapper(new Date(ts)); + } + } + + // map of app. size of time units in milliseconds + + var timeUnitSize = { + "second": 1000, + "minute": 60 * 1000, + "hour": 60 * 60 * 1000, + "day": 24 * 60 * 60 * 1000, + "month": 30 * 24 * 60 * 60 * 1000, + "quarter": 3 * 30 * 24 * 60 * 60 * 1000, + "year": 365.2425 * 24 * 60 * 60 * 1000 + }; + + // the allowed tick sizes, after 1 year we use + // an integer algorithm + + var baseSpec = [ + [1, "second"], [2, "second"], [5, "second"], [10, "second"], + [30, "second"], + [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], + [30, "minute"], + [1, "hour"], [2, "hour"], [4, "hour"], + [8, "hour"], [12, "hour"], + [1, "day"], [2, "day"], [3, "day"], + [0.25, "month"], [0.5, "month"], [1, "month"], + [2, "month"] + ]; + + // we don't know which variant(s) we'll need yet, but generating both is + // cheap + + var specMonths = baseSpec.concat([[3, "month"], [6, "month"], + [1, "year"]]); + var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], + [1, "year"]]); + + function init(plot) { + plot.hooks.processOptions.push(function (plot, options) { + $.each(plot.getAxes(), function(axisName, axis) { + + var opts = axis.options; + + if (opts.mode == "time") { + axis.tickGenerator = function(axis) { + + var ticks = []; + var d = dateGenerator(axis.min, opts); + var minSize = 0; + + // make quarter use a possibility if quarters are + // mentioned in either of these options + + var spec = (opts.tickSize && opts.tickSize[1] === + "quarter") || + (opts.minTickSize && opts.minTickSize[1] === + "quarter") ? specQuarters : specMonths; + + if (opts.minTickSize != null) { + if (typeof opts.tickSize == "number") { + minSize = opts.tickSize; + } else { + minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; + } + } + + for (var i = 0; i < spec.length - 1; ++i) { + if (axis.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) { + break; + } + } + + var size = spec[i][0]; + var unit = spec[i][1]; + + // special-case the possibility of several years + + if (unit == "year") { + + // if given a minTickSize in years, just use it, + // ensuring that it's an integer + + if (opts.minTickSize != null && opts.minTickSize[1] == "year") { + size = Math.floor(opts.minTickSize[0]); + } else { + + var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); + var norm = (axis.delta / timeUnitSize.year) / magn; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + } + + // minimum size for years is 1 + + if (size < 1) { + size = 1; + } + } + + axis.tickSize = opts.tickSize || [size, unit]; + var tickSize = axis.tickSize[0]; + unit = axis.tickSize[1]; + + var step = tickSize * timeUnitSize[unit]; + + if (unit == "second") { + d.setSeconds(floorInBase(d.getSeconds(), tickSize)); + } else if (unit == "minute") { + d.setMinutes(floorInBase(d.getMinutes(), tickSize)); + } else if (unit == "hour") { + d.setHours(floorInBase(d.getHours(), tickSize)); + } else if (unit == "month") { + d.setMonth(floorInBase(d.getMonth(), tickSize)); + } else if (unit == "quarter") { + d.setMonth(3 * floorInBase(d.getMonth() / 3, + tickSize)); + } else if (unit == "year") { + d.setFullYear(floorInBase(d.getFullYear(), tickSize)); + } + + // reset smaller components + + d.setMilliseconds(0); + + if (step >= timeUnitSize.minute) { + d.setSeconds(0); + } + if (step >= timeUnitSize.hour) { + d.setMinutes(0); + } + if (step >= timeUnitSize.day) { + d.setHours(0); + } + if (step >= timeUnitSize.day * 4) { + d.setDate(1); + } + if (step >= timeUnitSize.month * 2) { + d.setMonth(floorInBase(d.getMonth(), 3)); + } + if (step >= timeUnitSize.quarter * 2) { + d.setMonth(floorInBase(d.getMonth(), 6)); + } + if (step >= timeUnitSize.year) { + d.setMonth(0); + } + + var carry = 0; + var v = Number.NaN; + var prev; + + do { + + prev = v; + v = d.getTime(); + ticks.push(v); + + if (unit == "month" || unit == "quarter") { + if (tickSize < 1) { + + // a bit complicated - we'll divide the + // month/quarter up but we need to take + // care of fractions so we don't end up in + // the middle of a day + + d.setDate(1); + var start = d.getTime(); + d.setMonth(d.getMonth() + + (unit == "quarter" ? 3 : 1)); + var end = d.getTime(); + d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); + carry = d.getHours(); + d.setHours(0); + } else { + d.setMonth(d.getMonth() + + tickSize * (unit == "quarter" ? 3 : 1)); + } + } else if (unit == "year") { + d.setFullYear(d.getFullYear() + tickSize); + } else { + d.setTime(v + step); + } + } while (v < axis.max && v != prev); + + return ticks; + }; + + axis.tickFormatter = function (v, axis) { + + var d = dateGenerator(v, axis.options); + + // first check global format + + if (opts.timeformat != null) { + return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); + } + + // possibly use quarters if quarters are mentioned in + // any of these places + + var useQuarters = (axis.options.tickSize && + axis.options.tickSize[1] == "quarter") || + (axis.options.minTickSize && + axis.options.minTickSize[1] == "quarter"); + + var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; + var span = axis.max - axis.min; + var suffix = (opts.twelveHourClock) ? " %p" : ""; + var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; + var fmt; + + if (t < timeUnitSize.minute) { + fmt = hourCode + ":%M:%S" + suffix; + } else if (t < timeUnitSize.day) { + if (span < 2 * timeUnitSize.day) { + fmt = hourCode + ":%M" + suffix; + } else { + fmt = "%b %d " + hourCode + ":%M" + suffix; + } + } else if (t < timeUnitSize.month) { + fmt = "%b %d"; + } else if ((useQuarters && t < timeUnitSize.quarter) || + (!useQuarters && t < timeUnitSize.year)) { + if (span < timeUnitSize.year) { + fmt = "%b"; + } else { + fmt = "%b %Y"; + } + } else if (useQuarters && t < timeUnitSize.year) { + if (span < timeUnitSize.year) { + fmt = "Q%q"; + } else { + fmt = "Q%q %Y"; + } + } else { + fmt = "%Y"; + } + + var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); + + return rt; + }; + } + }); + }); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'time', + version: '1.0' + }); + + // Time-axis support used to be in Flot core, which exposed the + // formatDate function on the plot object. Various plugins depend + // on the function, so we need to re-expose it here. + + $.plot.formatDate = formatDate; + +})(jQuery);