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);