From f66c9ae3d625c18e4a3d09ca8ccc97ee4f28bee9 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Mon, 24 Dec 2012 18:44:18 -0500 Subject: [PATCH 01/24] Renamed the 'canvas' variable to 'surface'. Renaming the variable gives us room to create a new class called Canvas. --- jquery.flot.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/jquery.flot.js b/jquery.flot.js index 918352a..157d9d9 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -148,7 +148,7 @@ Licensed under the MIT license. }, 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, @@ -175,7 +175,7 @@ Licensed under the MIT license. plot.setupGrid = setupGrid; plot.draw = draw; plot.getPlaceholder = function() { return placeholder; }; - plot.getCanvas = function() { return canvas; }; + plot.getCanvas = function() { return surface; }; plot.getPlotOffset = function() { return plotOffset; }; plot.width = function () { return plotWidth; }; plot.height = function () { return plotHeight; }; @@ -211,7 +211,7 @@ Licensed under the MIT license. plot.shutdown = shutdown; plot.resize = function () { getCanvasDimensions(); - resizeCanvas(canvas); + resizeCanvas(surface); resizeCanvas(overlay); }; @@ -837,10 +837,10 @@ Licensed under the MIT license. function setupCanvases() { var reused, - existingCanvas = placeholder.children("canvas.flot-base"), + existingSurface = placeholder.children("canvas.flot-base"), existingOverlay = placeholder.children("canvas.flot-overlay"); - if (existingCanvas.length == 0 || existingOverlay == 0) { + if (existingSurface.length == 0 || existingOverlay == 0) { // init everything placeholder.html(""); // make sure placeholder is clear @@ -852,7 +852,7 @@ Licensed under the MIT license. getCanvasDimensions(); - canvas = makeCanvas("flot-base"); + surface = makeCanvas("flot-base"); overlay = makeCanvas("flot-overlay"); // overlay canvas for interactive features reused = false; @@ -860,13 +860,13 @@ Licensed under the MIT license. else { // reuse existing elements - canvas = existingCanvas.get(0); + surface = existingSurface.get(0); overlay = existingOverlay.get(0); reused = true; } - ctx = canvas.getContext("2d"); + ctx = surface.getContext("2d"); octx = overlay.getContext("2d"); // define which element we're listening for events on @@ -884,7 +884,7 @@ Licensed under the MIT license. // then whack any remaining obvious garbage left eventHolder.unbind(); - placeholder.children().not([canvas, overlay]).remove(); + placeholder.children().not([surface, overlay]).remove(); } // save in case we get replotted From a9be4d559da2507c15f5a8231489c178dbfc47bc Mon Sep 17 00:00:00 2001 From: David Schnur Date: Mon, 24 Dec 2012 20:01:07 -0500 Subject: [PATCH 02/24] Abstract-out canvas creation into an object. Moved canvas creation and size management into a new Canvas class. This is the first step towards a more object-oriented architecture. Since we create multiple canvases, and have to maintain several module-global variables to track their properties, they are the ideal place to start. This commit also removes sizing code that was duplicated between makeCanvas and resizeCanvas. --- jquery.flot.js | 270 +++++++++++++++++++++++++------------------------ 1 file changed, 140 insertions(+), 130 deletions(-) diff --git a/jquery.flot.js b/jquery.flot.js index 157d9d9..0e15407 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -33,6 +33,121 @@ Licensed under the MIT license. // the actual Flot code (function($) { + + /////////////////////////////////////////////////////////////////////////// + // 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 = document.createElement("canvas"); + element.className = cls; + + $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) + .data("canvas", this) + .appendTo(container); + + // If HTML5 Canvas isn't available, fall back to Excanvas + + 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."); + } + } + + var context = element.getContext("2d"); + + this.element = element; + this.context = context; + + // 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()); + }; + + // 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. + + Canvas.prototype.clear = function() { + this.context.clearRect(0, 0, this.width, this.height); + } + + /////////////////////////////////////////////////////////////////////////// + // The top-level container for the entire plot. + function Plot(placeholder, data_, options_, plugins) { // data is on the form: // [ series1, series2 ... ] @@ -154,7 +269,6 @@ Licensed under the MIT license. 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: [], @@ -175,7 +289,7 @@ Licensed under the MIT license. plot.setupGrid = setupGrid; plot.draw = draw; plot.getPlaceholder = function() { return placeholder; }; - plot.getCanvas = function() { return surface; }; + plot.getCanvas = function() { return surface.element; }; plot.getPlotOffset = function() { return plotOffset; }; plot.width = function () { return plotWidth; }; plot.height = function () { return plotHeight; }; @@ -210,9 +324,10 @@ Licensed under the MIT license. }; plot.shutdown = shutdown; plot.resize = function () { - getCanvasDimensions(); - resizeCanvas(surface); - resizeCanvas(overlay); + var width = placeholder.width(), + height = placeholder.height(); + surface.resize(width, height); + overlay.resize(width, height); }; // public attributes @@ -730,111 +845,6 @@ Licensed under the MIT license. }); } - ////////////////////////////////////////////////////////////////////////////////// - // 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(cls) { - - var c = document.createElement('canvas'); - c.className = cls; - - $(c).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) - .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, existingSurface = placeholder.children("canvas.flot-base"), @@ -850,27 +860,25 @@ Licensed under the MIT license. if (placeholder.css("position") == 'static') placeholder.css("position", "relative"); // for positioning labels and overlay - getCanvasDimensions(); - - surface = makeCanvas("flot-base"); - overlay = makeCanvas("flot-overlay"); // overlay canvas for interactive features + surface = new Canvas("flot-base", placeholder); + overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features reused = false; } else { // reuse existing elements - surface = existingSurface.get(0); - overlay = existingOverlay.get(0); + surface = existingSurface.data("canvas"); + overlay = existingOverlay.data("canvas"); reused = true; } - ctx = surface.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); if (reused) { // run shutdown in the old plot object @@ -880,11 +888,12 @@ Licensed under the MIT license. plot.resize(); // make sure overlay pixels are cleared (canvas is cleared when we redraw) - octx.clearRect(0, 0, canvasWidth, canvasHeight); + + overlay.clear(); // then whack any remaining obvious garbage left eventHolder.unbind(); - placeholder.children().not([surface, overlay]).remove(); + placeholder.children().not([surface.element, overlay.element]).remove(); } // save in case we get replotted @@ -1053,7 +1062,7 @@ Licensed under the MIT license. 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 }; @@ -1069,7 +1078,7 @@ Licensed under the MIT license. } else { plotOffset.right += lw + axisMargin; - axis.box = { left: canvasWidth - plotOffset.right, width: lw }; + axis.box = { left: surface.width - plotOffset.right, width: lw }; } } @@ -1085,11 +1094,11 @@ Licensed under the MIT license. // 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; } } @@ -1198,8 +1207,8 @@ Licensed under the MIT license. }); } - 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) { @@ -1258,7 +1267,7 @@ Licensed under the MIT license. 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; @@ -1429,7 +1438,8 @@ Licensed under the MIT license. } function draw() { - ctx.clearRect(0, 0, canvasWidth, canvasHeight); + + surface.clear(); executeHooks(hooks.drawBackground, [ctx]); @@ -2543,7 +2553,7 @@ Licensed under the MIT license. // draw highlights octx.save(); - octx.clearRect(0, 0, canvasWidth, canvasHeight); + overlay.clear(); octx.translate(plotOffset.left, plotOffset.top); var i, hi; From 42d5592add1d96ec93a37d62a6b49dd88ce1d94e Mon Sep 17 00:00:00 2001 From: David Schnur Date: Sun, 13 Jan 2013 10:08:06 -0500 Subject: [PATCH 03/24] Added a basic frame for the canvas-drawing plugin. --- jquery.flot.canvas.js | 68 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 jquery.flot.canvas.js diff --git a/jquery.flot.canvas.js b/jquery.flot.canvas.js new file mode 100644 index 0000000..8a00683 --- /dev/null +++ b/jquery.flot.canvas.js @@ -0,0 +1,68 @@ +/* Flot plugin for drawing all elements of a plot on the canvas. + +Copyright (c) 2007-2012 IOLA and Ole Laursen. +Licensed under the MIT license. + +Flot normally produces certain elements, like axis labels and the legend, using +HTML elements. This permits greater interactivity and customization, and often +looks better, due to cross-browser canvas text inconsistencies and limitations. + +It can also be desirable to render the plot entirely in canvas, particularly +if the goal is to save it as an image, or if Flot is being used in a context +where the HTML DOM does not exist, as is the case within Node.js. This plugin +switches out Flot's standard drawing operations for canvas-only replacements. + +Currently the plugin supports only axis labels, but it will eventually allow +every element of the plot to be rendered directly to canvas. + +The plugin supports these options: + + canvas: boolean, + xaxis, yaxis: { + font: null or font spec object + } + +The top-level "canvas" option controls whether full canvas drawing is enabled, +making it easy to toggle on and off. + +By default the plugin extracts font settings from the same CSS styles that the +default HTML text implementation uses. If *.tickLabel* has a *font-size* of +20px, then the canvas text will be drawn at the same size. + +One can also use the "font" option to control these properties directly. The +format of the font spec object is as follows: + + { + size: 11, + style: "italic", + weight: "bold", + family: "sans-serif", + variant: "small-caps" + } + +*/ + +(function($) { + + var options = { + canvas: true, + xaxis: { + font: null + }, + yaxis: { + font: null + } + }; + + function init(plot) { + + } + + $.plot.plugins.push({ + init: init, + options: options, + name: "canvas", + version: "1.0" + }); + +})(jQuery); From edc2bbd9922999ddcde3303bfdb629cda8a46f09 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Fri, 18 Jan 2013 16:08:14 -0500 Subject: [PATCH 04/24] Added methods to draw and measure text. These methods provide a common way to draw HTML text above a canvas. The getTextInfo method generates div HTML for text with a given font style/class and angle, measures the element's dimensions, and saves everything in a cache. The drawText method takes the resulting entry, finishes generating the inline styles necessary to position the div, and adds the result to a buffer. The render method dumps the buffer into an overlay and expires unused cache entries. --- jquery.flot.js | 197 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 194 insertions(+), 3 deletions(-) diff --git a/jquery.flot.js b/jquery.flot.js index 0e15407..f9c5b7d 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -46,8 +46,11 @@ Licensed under the MIT license. function Canvas(cls, container) { + this.container = container; + var element = document.createElement("canvas"); element.className = cls; + this.element = element; $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) .data("canvas", this) @@ -64,8 +67,6 @@ Licensed under the MIT license. } var context = element.getContext("2d"); - - this.element = element; this.context = context; // Determine the screen's ratio of physical to device-independent @@ -89,6 +90,31 @@ Licensed under the MIT license. // Size the canvas to match the internal dimensions of its container this.resize(container.width(), container.height()); + + // Container for HTML text overlaid onto the canvas; created on demand + + this.text = null; + + // Buffer for HTML text fragments, so we can add them all at once + + this._textBuffer = ""; + + // 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 = {}; + + // A 'hot' copy of the text cache; it holds only info that has been + // accessed in the past render cycle. With each render it is saved as + // the new text cache, as an alternative to more complicated ways of + // expiring items that are no longer needed. + + // NOTE: It's unclear how this compares performance-wise to keeping a + // single cache and looping over it to delete expired items. This way + // is certainly less operations, but seems like it might result in more + // garbage collection and possibly increased cache-insert times. + + this._activeTextCache = {}; }; // Resizes the canvas to the given dimensions. @@ -139,10 +165,175 @@ Licensed under the MIT license. context.scale(pixelRatio, pixelRatio); } - // Clears the entire canvas area. + // Clears the entire canvas area, including overlaid text. Canvas.prototype.clear = function() { this.context.clearRect(0, 0, this.width, this.height); + if (this.text) { + this.text.empty(); + } + } + + // Finishes rendering the canvas, including populating the text overlay. + + Canvas.prototype.render = function() { + + if (this._textBuffer.length) { + + // Add the HTML text layer, if it doesn't already exist + + if (!this.text) { + this.text = $("
").css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + }).insertAfter(this.element); + } + + this.text.append(this._textBuffer); + this._textBuffer = ""; + } + + // Swap out the text cache for the 'hot cache' that we've been filling + // out since the last call to render. + + this._textCache = this._activeTextCache; + this._activeTextCache = {}; + } + + // Creates (if necessary) and returns a text info object. + // + // The object looks like this: + // + // { + // prefix: First half of the HTML for the text's div wrapper. + // suffix: Second half of the HTML for the text's div wrapper. + // dimensions: { + // width: Width of the text's wrapper div. + // height: Height of the text's wrapper div. + // } + // } + // + // The prefix and suffix are divided at the 'top' inline style definition, + // so the top and left positions can be added when creating the real div. + // + // Canvas maintains a cache of recently-used text info objects; getTextInfo + // either returns the cached element or creates a new entry. + // + // @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. + // @return {object} a text info object. + + Canvas.prototype.getTextInfo = function(text, font, angle) { + + var textStyle, cacheKey, 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.family; + } else { + textStyle = font; + } + + // The text + style + angle uniquely identify the text's dimensions and + // content; we'll use them to build this entry's text cache key. + + cacheKey = text + "-" + textStyle + "-" + angle; + + info = this._textCache[cacheKey] || this._activeTextCache[cacheKey]; + + if (info == null) { + + var prefix, + suffix = "px;'>" + text + "", + element; + + // If the font is a font-spec object, generate an inline-style string + + if (typeof font === "object") { + prefix = "
Date: Fri, 18 Jan 2013 16:45:28 -0500 Subject: [PATCH 05/24] Provide a way for plugins to override classes. --- jquery.flot.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/jquery.flot.js b/jquery.flot.js index f9c5b7d..d6f3306 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -541,9 +541,16 @@ Licensed under the MIT license. } 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); } From a0529ee8b18d1b6dfe2079f528f999c3451252ff Mon Sep 17 00:00:00 2001 From: David Schnur Date: Fri, 18 Jan 2013 23:26:56 -0500 Subject: [PATCH 06/24] Moved canvas tick rendering into a plugin. The base implementation uses the new drawText and getTextInfo methods to draw text in HTML. Canvas rendering has been moved to overrides of these methods within the canvas-render plugin. --- jquery.flot.canvas.js | 262 +++++++++++++++++++++++++++++++++++++----- jquery.flot.js | 137 ++++++++-------------- 2 files changed, 280 insertions(+), 119 deletions(-) diff --git a/jquery.flot.canvas.js b/jquery.flot.canvas.js index 8a00683..36e1873 100644 --- a/jquery.flot.canvas.js +++ b/jquery.flot.canvas.js @@ -17,45 +17,249 @@ every element of the plot to be rendered directly to canvas. The plugin supports these options: - canvas: boolean, - xaxis, yaxis: { - font: null or font spec object - } +{ + canvas: boolean +} -The top-level "canvas" option controls whether full canvas drawing is enabled, -making it easy to toggle on and off. - -By default the plugin extracts font settings from the same CSS styles that the -default HTML text implementation uses. If *.tickLabel* has a *font-size* of -20px, then the canvas text will be drawn at the same size. - -One can also use the "font" option to control these properties directly. The -format of the font spec object is as follows: - - { - size: 11, - style: "italic", - weight: "bold", - family: "sans-serif", - variant: "small-caps" - } +The "canvas" option controls whether full canvas drawing is enabled, making it +possible to toggle on and off. This is useful when a plot uses HTML text in the +browser, but needs to redraw with canvas text when exporting as an image. */ (function($) { var options = { - canvas: true, - xaxis: { - font: null - }, - yaxis: { - font: null - } + canvas: true }; - function init(plot) { + function init(plot, classes) { + + var Canvas = classes.Canvas, + getTextInfo = Canvas.prototype.getTextInfo, + drawText = Canvas.prototype.drawText; + + // Creates (if necessary) and returns a text info object. + // + // When the canvas option is set, this override returns an object + // that looks like this: + // + // { + // lines: { + // height: Height of each line in the text. + // widths: List of widths for each line in the text. + // texts: List of lines in the text. + // }, + // font: { + // definition: Canvas font property string. + // color: Color of the text. + // }, + // dimensions: { + // width: Width of the text's bounding box. + // height: Height of the text's bounding box. + // } + // } + + Canvas.prototype.getTextInfo = function(text, font, angle) { + if (plot.getOptions().canvas) { + + var textStyle, cacheKey, info; + + // Cast the value to a string, in case we were given a number + + text = "" + text; + + // If the font is a font-spec object, generate a CSS definition + + if (typeof font === "object") { + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; + } else { + textStyle = font; + } + + // The text + style + angle uniquely identify the text's + // dimensions and content; we'll use them to build this entry's + // text cache key. + + cacheKey = text + "-" + textStyle + "-" + angle; + + info = this._textCache[cacheKey] || this._activeTextCache[cacheKey]; + + if (info == null) { + + var context = this.context; + + // If the font was provided as CSS, create a div with those + // classes and examine it to generate a canvas font spec. + + if (typeof font !== "object") { + + var element; + if (typeof font === "string") { + element = $("
" + text + "
") + .appendTo(this.container); + } else { + element = $("
" + text + "
") + .appendTo(this.container); + } + + font = { + style: element.css("font-style"), + variant: element.css("font-variant"), + weight: element.css("font-weight"), + size: parseInt(element.css("font-size")), + family: element.css("font-family"), + color: element.css("color") + }; + + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; + + element.remove(); + } + + // Create a new info object, initializing the dimensions to + // zero so we can count them up line-by-line. + + info = { + lines: [], + font: { + definition: textStyle, + color: font.color + }, + dimensions: { + width: 0, + height: 0 + } + }; + + context.save(); + context.font = textStyle; + + // Canvas can't handle multi-line strings; break on various + // newlines, including HTML brs, to build a list of lines. + // Note that we could split directly on regexps, but IE < 9 + // is broken; revisit when we drop IE 7/8 support. + + var lines = (text + "").replace(/
|\r\n|\r/g, "\n").split("\n"); + + for (var i = 0; i < lines.length; ++i) { + + var lineText = lines[i], + measured = context.measureText(lineText), + lineWidth, lineHeight; + + lineWidth = measured.width; + + // Height might not be defined; not in the standard yet + lineHeight = measured.height || font.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. + + lineHeight += Math.round(font.size * 0.15); + + info.dimensions.width = Math.max(lineWidth, info.dimensions.width); + info.dimensions.height += lineHeight; + + info.lines.push({ + text: lineText, + width: lineWidth, + height: lineHeight + }); + } + + context.restore; + } + + // Save the entry to the 'hot' text cache, marking it as active + // and preserving it for the next render pass. + + this._activeTextCache[cacheKey] = info; + + return info; + + } else { + return getTextInfo.call(this, text, font, angle); + } + } + + // Draws a text string onto the canvas. + // + // When the canvas option is set, this override draws directly to the + // canvas using fillText. + + Canvas.prototype.drawText = function(x, y, text, font, angle, halign, valign) { + if (plot.getOptions().canvas) { + + var info = this.getTextInfo(text, font, angle), + dimensions = info.dimensions, + context = this.context, + lines = info.lines; + + // Apply alignment to the vertical position of the entire text + + if (valign == "middle") { + y -= dimensions.height / 2; + } else if (valign == "bottom") { + y -= dimensions.height; + } + + context.save(); + + context.fillStyle = info.font.color; + context.font = info.font.definition; + + // TODO: Comments in Ole's implementation indicate that some + // browsers differ in their interpretation of 'top'; so far I + // don't see this, but it requires more testing. We'll stick + // with top until this can be verified. Original comment was: + + // Top alignment 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. + + context.textBaseline = "top"; + + for (var i = 0; i < lines.length; ++i) { + + var line = lines[i], + linex = x; + + // Apply alignment to the horizontal position per-line + + if (halign == "center") { + linex -= line.width / 2; + } else if (halign == "right") { + linex -= line.width; + } + + // FIXME: LEGACY BROWSER FIX + // AFFECTS: Opera < 12.00 + + // 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 + + if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { + linex = Math.floor(linex); + y = Math.ceil(y - 2); + } + + context.fillText(line.text, linex, y); + y += line.height; + } + + context.restore(); + + } else { + drawText.call(this, x, y, text, font, angle, halign, valign); + } + } } $.plot.plugins.push({ diff --git a/jquery.flot.js b/jquery.flot.js index d6f3306..6571598 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -34,6 +34,12 @@ Licensed under the MIT license. // the actual Flot code (function($) { + // Add default styles for tick labels and other text + + $(function() { + $("head").prepend(""); + }); + /////////////////////////////////////////////////////////////////////////// // The Canvas object is a wrapper around an HTML5 tag. // @@ -1091,7 +1097,7 @@ Licensed under the MIT license. // then whack any remaining obvious garbage left eventHolder.unbind(); - placeholder.children().not([surface.element, overlay.element]).remove(); + placeholder.children().not([surface.element, surface.text, overlay.element, overlay.text]).remove(); } // save in case we get replotted @@ -1163,53 +1169,26 @@ Licensed under the MIT license. } 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 + "'"; + font = axis.font || "flot-tick-label flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis"; for (var i = 0; i < ticks.length; ++i) { - var t = ticks[i]; - t.lines = []; - t.width = t.height = 0; + var t = ticks[i], + dimensions; if (!t.label) continue; - // accept various kinds of newlines, including HTML ones - // (you can actually split directly on regexps in Javascript, - // but IE < 9 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); - - 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); - } + dimensions = surface.getTextInfo(t.label, font).dimensions; if (opts.labelWidth == null) - axisw = Math.max(axisw, t.width); + axisw = Math.max(axisw, dimensions.width); if (opts.labelHeight == null) - axish = Math.max(axish, t.height); + axish = Math.max(axish, dimensions.height); } - ctx.restore(); axis.labelWidth = Math.ceil(axisw); axis.labelHeight = Math.ceil(axish); @@ -1368,7 +1347,7 @@ Licensed under the MIT license. }); 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)), @@ -1385,8 +1364,14 @@ Licensed under the MIT license. setTicks(axis); snapRangeToTicks(axis, axis.ticks); + // If a font-spec object was provided, use font defaults + // to fill out any unspecified settings. + + if (axis.font) { + axis.font = $.extend({}, fontDefaults, axis.options.font); + } + // find labelWidth/Height for axis - axis.font = $.extend({}, fontDefaults, axis.options.font); measureTickLabels(axis); }); @@ -1663,6 +1648,8 @@ Licensed under the MIT license. drawGrid(); drawAxisLabels(); } + + surface.render(); } function extractRange(ranges, coord) { @@ -1934,74 +1921,44 @@ Licensed under the MIT license. } 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 - - 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"; + var box = axis.box, + font = axis.font || "flot-tick-label flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis", + tick, x, y, halign, valign; 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; - } - 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; + 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"; } - - // account for middle aligning and line number - y += line.height/2 + offset; - offset += line.height; - - if (!!(window.opera && window.opera.version().split('.')[0] < 12)) { - // FIXME: LEGACY BROWSER FIX - // AFFECTS: Opera < 12.00 - - // 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); + } 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; } - ctx.fillText(line.text, x, y); } + + surface.drawText(x, y, tick.label, font, null, halign, valign); } }); - - ctx.restore(); } function drawSeries(series) { From c36b3446774553e4bd2f68d48c3ce607edebe81f Mon Sep 17 00:00:00 2001 From: David Schnur Date: Wed, 30 Jan 2013 20:49:29 -0500 Subject: [PATCH 07/24] Replace axis.font with options.font. Instead of giving the axis its own font property, we simply look at its options, where the font comes from in the first place. A separate property is unnecessary and inconsistent with the way other axis options are handled. --- jquery.flot.js | 59 +++++++++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/jquery.flot.js b/jquery.flot.js index 6571598..410c01e 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -563,7 +563,6 @@ Licensed under the MIT license. } function parseOptions(opts) { - var i; $.extend(true, options, opts); @@ -582,12 +581,38 @@ Licensed under the MIT license. 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") + }; + + axisCount = options.xaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + axisOptions = $.extend(true, {}, options.xaxis, options.xaxes[i]); + options.xaxes[i] = axisOptions; + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + } + } + + axisCount = options.yaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + axisOptions = $.extend(true, {}, options.yaxis, options.yaxes[i]); + options.yaxes[i] = axisOptions; + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + } + } // backwards compatibility, to be removed in future if (options.xaxis.noTicks && options.xaxis.ticks == null) @@ -1172,7 +1197,7 @@ Licensed under the MIT license. var opts = axis.options, ticks = axis.ticks || [], axisw = opts.labelWidth || 0, axish = opts.labelHeight || 0, - font = axis.font || "flot-tick-label flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis"; + font = opts.font || "flot-tick-label flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis"; for (var i = 0; i < ticks.length; ++i) { @@ -1348,14 +1373,6 @@ Licensed under the MIT license. if (showGrid) { - 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; }); $.each(allocatedAxes, function (_, axis) { @@ -1363,14 +1380,6 @@ Licensed under the MIT license. setupTickGeneration(axis); setTicks(axis); snapRangeToTicks(axis, axis.ticks); - - // If a font-spec object was provided, use font defaults - // to fill out any unspecified settings. - - if (axis.font) { - axis.font = $.extend({}, fontDefaults, axis.options.font); - } - // find labelWidth/Height for axis measureTickLabels(axis); }); @@ -1927,7 +1936,7 @@ Licensed under the MIT license. return; var box = axis.box, - font = axis.font || "flot-tick-label flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis", + font = axis.options.font || "flot-tick-label flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis", tick, x, y, halign, valign; for (var i = 0; i < axis.ticks.length; ++i) { From d2642e80cf066a96e67e5e2b44505173048c8a54 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Wed, 30 Jan 2013 21:21:37 -0500 Subject: [PATCH 08/24] Fixed missing/superfluous semicolons. --- jquery.flot.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/jquery.flot.js b/jquery.flot.js index 410c01e..3e94cfe 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -121,7 +121,7 @@ Licensed under the MIT license. // garbage collection and possibly increased cache-insert times. this._activeTextCache = {}; - }; + } // Resizes the canvas to the given dimensions. // @@ -169,7 +169,7 @@ Licensed under the MIT license. // appear at the same size; the extra pixels will just make them crisper. context.scale(pixelRatio, pixelRatio); - } + }; // Clears the entire canvas area, including overlaid text. @@ -178,7 +178,7 @@ Licensed under the MIT license. if (this.text) { this.text.empty(); } - } + }; // Finishes rendering the canvas, including populating the text overlay. @@ -207,7 +207,7 @@ Licensed under the MIT license. this._textCache = this._activeTextCache; this._activeTextCache = {}; - } + }; // Creates (if necessary) and returns a text info object. // @@ -297,7 +297,7 @@ Licensed under the MIT license. this._activeTextCache[cacheKey] = info; return info; - } + }; // Draws a text string onto the canvas. // @@ -340,7 +340,7 @@ Licensed under the MIT license. // HTML to the canvas text buffer. this._textBuffer += info.prefix + parseInt(y) + "px;left:" + parseInt(x) + info.suffix; - } + }; /////////////////////////////////////////////////////////////////////////// // The top-level container for the entire plot. @@ -1514,7 +1514,7 @@ Licensed under the MIT license. axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); axis.tickSize = opts.tickSize || size; - start = floorInBase(axis.min, axis.tickSize) + start = floorInBase(axis.min, axis.tickSize); do { prev = v; From 98b6361aa9dd3ead1f032f01ea6bfe6d75631bf2 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Wed, 30 Jan 2013 21:47:05 -0500 Subject: [PATCH 09/24] Preserve canvas elements on re-plot. Since the Canvas .text object is jQuery-wrapped, it was not preserved as expected when clearing the canvas of junk. I've replaced the selection with one based on element classes. --- jquery.flot.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/jquery.flot.js b/jquery.flot.js index 3e94cfe..5562bd7 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -189,13 +189,15 @@ Licensed under the MIT license. // Add the HTML text layer, if it doesn't already exist if (!this.text) { - this.text = $("
").css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0 - }).insertAfter(this.element); + this.text = $("
") + .addClass("flot-text") + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + }).insertAfter(this.element); } this.text.append(this._textBuffer); @@ -1122,7 +1124,7 @@ Licensed under the MIT license. // then whack any remaining obvious garbage left eventHolder.unbind(); - placeholder.children().not([surface.element, surface.text, overlay.element, overlay.text]).remove(); + placeholder.children(":not(.flot-base,.flot-overlay,.flot-text)").remove(); } // save in case we get replotted From 73baa2b9e352517f1c503d7eec2f0cfc4b8ce078 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Sat, 16 Feb 2013 17:27:24 -0500 Subject: [PATCH 10/24] Cache actual elements instead of buffering HTML. This significantly improves performance, since we already create the elements when measuring them, and that effort is now no longer wasted. We must take care to detach, rather than remove, when clearing the text layer, so we can add the elements back later if necessary. --- jquery.flot.js | 102 +++++++++++++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 38 deletions(-) diff --git a/jquery.flot.js b/jquery.flot.js index 5562bd7..292de37 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -101,10 +101,6 @@ Licensed under the MIT license. this.text = null; - // Buffer for HTML text fragments, so we can add them all at once - - this._textBuffer = ""; - // Cache of text fragments and metrics, so we can avoid expensively // re-calculating them when the plot is re-rendered in a loop. @@ -176,7 +172,7 @@ Licensed under the MIT license. Canvas.prototype.clear = function() { this.context.clearRect(0, 0, this.width, this.height); if (this.text) { - this.text.empty(); + this.text.html(""); } }; @@ -184,9 +180,32 @@ Licensed under the MIT license. Canvas.prototype.render = function() { - if (this._textBuffer.length) { + var cache = this._activeTextCache; + + // Swap out the text cache for the 'hot cache' that we've been filling + // out since the last call to render. + + this._activeTextCache = {}; + this._textCache = cache; + + // Check whether the cache actually has any entries. + + var hasOwnProperty = Object.prototype.hasOwnProperty, + cacheHasText = false; + + for (var key in cache) { + if (hasOwnProperty.call(cache, key)) { + cacheHasText = true; + break; + } + } + + // Render the contents of the cache - // Add the HTML text layer, if it doesn't already exist + if (cacheHasText) { + + // Create the HTML text layer, if it doesn't already exist; if it + // does, detach it so we don't get repaints while adding elements. if (!this.text) { this.text = $("
") @@ -197,18 +216,22 @@ Licensed under the MIT license. left: 0, bottom: 0, right: 0 - }).insertAfter(this.element); + }); + } else { + this.text.detach(); } - this.text.append(this._textBuffer); - this._textBuffer = ""; - } + // Add all the elements to the text layer, then add it to the DOM + // at the end, so we only trigger a single redraw. - // Swap out the text cache for the 'hot cache' that we've been filling - // out since the last call to render. + for (var key in cache) { + if (hasOwnProperty.call(cache, key)) { + this.text.append(cache[key].element); + } + } - this._textCache = this._activeTextCache; - this._activeTextCache = {}; + this.text.insertAfter(this.element); + } }; // Creates (if necessary) and returns a text info object. @@ -216,17 +239,13 @@ Licensed under the MIT license. // The object looks like this: // // { - // prefix: First half of the HTML for the text's div wrapper. - // suffix: Second half of the HTML for the text's div wrapper. + // element: The jQuery-wrapped HTML div containing the text. // dimensions: { // width: Width of the text's wrapper div. // height: Height of the text's wrapper div. // } // } // - // The prefix and suffix are divided at the 'top' inline style definition, - // so the top and left positions can be added when creating the real div. - // // Canvas maintains a cache of recently-used text info objects; getTextInfo // either returns the cached element or creates a new entry. // @@ -260,37 +279,42 @@ Licensed under the MIT license. info = this._textCache[cacheKey] || this._activeTextCache[cacheKey]; - if (info == null) { + // If we can't find a matching element in our cache, create a new one - var prefix, - suffix = "px;'>" + text + "
", - element; + if (info == null) { - // If the font is a font-spec object, generate an inline-style string + var element = $("
" + text + "
"); if (typeof font === "object") { - prefix = "
Date: Sat, 16 Feb 2013 19:09:21 -0500 Subject: [PATCH 11/24] Move cached hasOwnProperty to the top level. --- jquery.flot.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/jquery.flot.js b/jquery.flot.js index 292de37..c6a8c8f 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -40,6 +40,10 @@ Licensed under the MIT license. $("head").prepend(""); }); + // Cache the prototype hasOwnProperty for faster access + + var hasOwnProperty = Object.prototype.hasOwnProperty; + /////////////////////////////////////////////////////////////////////////// // The Canvas object is a wrapper around an HTML5 tag. // @@ -190,8 +194,7 @@ Licensed under the MIT license. // Check whether the cache actually has any entries. - var hasOwnProperty = Object.prototype.hasOwnProperty, - cacheHasText = false; + var cacheHasText = false; for (var key in cache) { if (hasOwnProperty.call(cache, key)) { From 5d7086968f73152ff5953c13af54ef822c4f0e50 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Wed, 20 Feb 2013 20:57:36 -0500 Subject: [PATCH 12/24] Simplify creation of the cached element. --- jquery.flot.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/jquery.flot.js b/jquery.flot.js index c6a8c8f..72f2ebe 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -286,25 +286,18 @@ Licensed under the MIT license. if (info == null) { - var element = $("
" + text + "
"); + var element = $("
").html(text).css({ + position: "absolute", + top: -9999 + }); if (typeof font === "object") { element.css({ font: textStyle, color: font.color, - position: "absolute", - top: -9999 }); } else if (typeof font === "string") { - element.addClass(font).css({ - position: "absolute", - top: -9999 - }); - } else { - element.css({ - position: "absolute", - top: -9999 - }); + element.addClass(font); } element.appendTo(this.container); From a9a31644c71aa39abc301b73ca2ab626f7faf874 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Sat, 23 Feb 2013 10:10:45 -0500 Subject: [PATCH 13/24] Replace drawText with add and remove methods. Every cache element now contains the actual text element instead of just its HTML, plus a flag indicating whether it is visible. The addText and removeText methods control the state of this flag, and the render method uses it to manage elements within the text container. So where we previously used drawText to actually render text, now we add each string once, then let the render method take care of drawing them as necessary. This dramatically improves performance by eliminating the need to clear and re-populate HTML text on every drawing cycle. Since the elements are now static between add/remove calls, this also allows users to add interactivity, as they could in 0.7. Finally, it eliminates the need for a separate 'hot' cache. I also removed the unnecessary 'dimensions' object; it's easier and faster to store the width and height at the top level of the info object. --- jquery.flot.canvas.js | 371 ++++++++++++++++++++++++------------------ jquery.flot.js | 195 ++++++++++++---------- 2 files changed, 317 insertions(+), 249 deletions(-) diff --git a/jquery.flot.canvas.js b/jquery.flot.canvas.js index 36e1873..b3367bc 100644 --- a/jquery.flot.canvas.js +++ b/jquery.flot.canvas.js @@ -33,233 +33,280 @@ browser, but needs to redraw with canvas text when exporting as an image. canvas: true }; + // Cache the prototype hasOwnProperty for faster access + + var hasOwnProperty = Object.prototype.hasOwnProperty; + function init(plot, classes) { var Canvas = classes.Canvas, getTextInfo = Canvas.prototype.getTextInfo, - drawText = Canvas.prototype.drawText; + addText = Canvas.prototype.addText, + render = Canvas.prototype.render; - // Creates (if necessary) and returns a text info object. - // - // When the canvas option is set, this override returns an object - // that looks like this: - // - // { - // lines: { - // height: Height of each line in the text. - // widths: List of widths for each line in the text. - // texts: List of lines in the text. - // }, - // font: { - // definition: Canvas font property string. - // color: Color of the text. - // }, - // dimensions: { - // width: Width of the text's bounding box. - // height: Height of the text's bounding box. - // } - // } + // Finishes rendering the canvas, including overlaid text - Canvas.prototype.getTextInfo = function(text, font, angle) { - if (plot.getOptions().canvas) { + Canvas.prototype.render = function() { - var textStyle, cacheKey, info; - - // Cast the value to a string, in case we were given a number + if (!plot.getOptions().canvas) { + return render.call(this); + } - text = "" + text; + var context = this.context, + cache = this._textCache, + cacheHasText = false, + key; - // If the font is a font-spec object, generate a CSS definition + // Check whether the cache actually has any entries. - if (typeof font === "object") { - textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; - } else { - textStyle = font; + for (key in cache) { + if (hasOwnProperty.call(cache, key)) { + cacheHasText = true; + break; } + } - // The text + style + angle uniquely identify the text's - // dimensions and content; we'll use them to build this entry's - // text cache key. + if (!cacheHasText) { + return; + } - cacheKey = text + "-" + textStyle + "-" + angle; + // Render the contents of the cache - info = this._textCache[cacheKey] || this._activeTextCache[cacheKey]; + context.save(); - if (info == null) { + for (key in cache) { + if (hasOwnProperty.call(cache, key)) { - var context = this.context; + var info = cache[key]; - // If the font was provided as CSS, create a div with those - // classes and examine it to generate a canvas font spec. + if (!info.active) { + delete cache[key]; + continue; + } - if (typeof font !== "object") { + var x = info.x, + y = info.y, + lines = info.lines, + halign = info.halign; - var element; - if (typeof font === "string") { - element = $("
" + text + "
") - .appendTo(this.container); - } else { - element = $("
" + text + "
") - .appendTo(this.container); - } + context.fillStyle = info.font.color; + context.font = info.font.definition; - font = { - style: element.css("font-style"), - variant: element.css("font-variant"), - weight: element.css("font-weight"), - size: parseInt(element.css("font-size")), - family: element.css("font-family"), - color: element.css("color") - }; + // TODO: Comments in Ole's implementation indicate that + // some browsers differ in their interpretation of 'top'; + // so far I don't see this, but it requires more testing. + // We'll stick with top until this can be verified. - textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; + // Original comment was: + // Top alignment 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. - element.remove(); - } + context.textBaseline = "top"; - // Create a new info object, initializing the dimensions to - // zero so we can count them up line-by-line. - - info = { - lines: [], - font: { - definition: textStyle, - color: font.color - }, - dimensions: { - width: 0, - height: 0 - } - }; - - context.save(); - context.font = textStyle; + for (var i = 0; i < lines.length; ++i) { - // Canvas can't handle multi-line strings; break on various - // newlines, including HTML brs, to build a list of lines. - // Note that we could split directly on regexps, but IE < 9 - // is broken; revisit when we drop IE 7/8 support. + var line = lines[i], + linex = x; - var lines = (text + "").replace(/
|\r\n|\r/g, "\n").split("\n"); + // Apply horizontal alignment per-line - for (var i = 0; i < lines.length; ++i) { + if (halign == "center") { + linex -= line.width / 2; + } else if (halign == "right") { + linex -= line.width; + } - var lineText = lines[i], - measured = context.measureText(lineText), - lineWidth, lineHeight; + // FIXME: LEGACY BROWSER FIX + // AFFECTS: Opera < 12.00 - lineWidth = measured.width; + // Round the coordinates, since Opera otherwise + // switches to uglier (probably non-hinted) rendering. + // Also offset the y coordinate, since Opera is off + // pretty consistently compared to the other browsers. - // Height might not be defined; not in the standard yet + if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { + linex = Math.floor(linex); + y = Math.ceil(y - 2); + } - lineHeight = measured.height || font.size; + context.fillText(line.text, linex, y); + y += line.height; + } + } + } - // 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. + context.restore(); + }; - lineHeight += Math.round(font.size * 0.15); + // Creates (if necessary) and returns a text info object. + // + // When the canvas option is set, the object looks like this: + // + // { + // x: X coordinate at which the text is located. + // x: Y coordinate at which the text is located. + // width: Width of the text's bounding box. + // height: Height of the text's bounding box. + // active: Flag indicating whether the text should be visible. + // lines: [{ + // height: Height of this line. + // widths: Width of this line. + // text: Text on this line. + // }], + // font: { + // definition: Canvas font property string. + // color: Color of the text. + // }, + // } - info.dimensions.width = Math.max(lineWidth, info.dimensions.width); - info.dimensions.height += lineHeight; + Canvas.prototype.getTextInfo = function(text, font, angle) { - info.lines.push({ - text: lineText, - width: lineWidth, - height: lineHeight - }); - } + if (!plot.getOptions().canvas) { + return getTextInfo.call(this, text, font, angle); + } - context.restore; - } + var textStyle, cacheKey, info; - // Save the entry to the 'hot' text cache, marking it as active - // and preserving it for the next render pass. + // Cast the value to a string, in case we were given a number - this._activeTextCache[cacheKey] = info; + text = "" + text; - return info; + // If the font is a font-spec object, generate a CSS definition + if (typeof font === "object") { + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; } else { - return getTextInfo.call(this, text, font, angle); + textStyle = font; } - } - // Draws a text string onto the canvas. - // - // When the canvas option is set, this override draws directly to the - // canvas using fillText. + // The text + style + angle uniquely identify the text's dimensions + // and content; we'll use them to build the entry's text cache key. - Canvas.prototype.drawText = function(x, y, text, font, angle, halign, valign) { - if (plot.getOptions().canvas) { + cacheKey = text + "-" + textStyle + "-" + angle; - var info = this.getTextInfo(text, font, angle), - dimensions = info.dimensions, - context = this.context, - lines = info.lines; + info = this._textCache[cacheKey]; - // Apply alignment to the vertical position of the entire text + if (info == null) { - if (valign == "middle") { - y -= dimensions.height / 2; - } else if (valign == "bottom") { - y -= dimensions.height; - } + var context = this.context; - context.save(); + // If the font was provided as CSS, create a div with those + // classes and examine it to generate a canvas font spec. + + if (typeof font !== "object") { - context.fillStyle = info.font.color; - context.font = info.font.definition; + var element = $("
").html(text) + .addClass(typeof font === "string" ? font : null) + .appendTo(this.container); - // TODO: Comments in Ole's implementation indicate that some - // browsers differ in their interpretation of 'top'; so far I - // don't see this, but it requires more testing. We'll stick - // with top until this can be verified. Original comment was: + font = { + style: element.css("font-style"), + variant: element.css("font-variant"), + weight: element.css("font-weight"), + size: parseInt(element.css("font-size"), 10), + family: element.css("font-family"), + color: element.css("color") + }; + + element.remove(); + } + + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; + + // Create a new info object, initializing the dimensions to + // zero so we can count them up line-by-line. + + info = { + x: null, + y: null, + width: 0, + height: 0, + active: false, + lines: [], + font: { + definition: textStyle, + color: font.color + } + }; + + context.save(); + context.font = textStyle; - // Top alignment 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. + // Canvas can't handle multi-line strings; break on various + // newlines, including HTML brs, to build a list of lines. + // Note that we could split directly on regexps, but IE < 9 is + // broken; revisit when we drop IE 7/8 support. - context.textBaseline = "top"; + var lines = (text + "").replace(/
|\r\n|\r/g, "\n").split("\n"); for (var i = 0; i < lines.length; ++i) { - var line = lines[i], - linex = x; + var lineText = lines[i], + measured = context.measureText(lineText), + lineWidth, lineHeight; - // Apply alignment to the horizontal position per-line + lineWidth = measured.width; - if (halign == "center") { - linex -= line.width / 2; - } else if (halign == "right") { - linex -= line.width; - } + // Height might not be defined; not in the standard yet - // FIXME: LEGACY BROWSER FIX - // AFFECTS: Opera < 12.00 + lineHeight = measured.height || font.size; - // 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 + // 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. - if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { - linex = Math.floor(linex); - y = Math.ceil(y - 2); - } + lineHeight += Math.round(font.size * 0.15); + + info.width = Math.max(lineWidth, info.width); + info.height += lineHeight; - context.fillText(line.text, linex, y); - y += line.height; + info.lines.push({ + text: lineText, + width: lineWidth, + height: lineHeight + }); } + this._textCache[cacheKey] = info; + context.restore(); + } - } else { - drawText.call(this, x, y, text, font, angle, halign, valign); + return info; + }; + + // Adds a text string to the canvas text overlay. + + Canvas.prototype.addText = function(x, y, text, font, angle, halign, valign) { + + if (!plot.getOptions().canvas) { + return addText.call(this, x, y, text, font, angle, halign, valign); + } + + var info = this.getTextInfo(text, font, angle); + + info.x = x; + info.y = y; + + // Mark the text for inclusion in the next render pass + + info.active = true; + + // Save horizontal alignment for later; we'll apply it per-line + + info.halign = halign; + + // Tweak the initial y-position to match vertical alignment + + if (valign == "middle") { + info.y = y - info.height / 2; + } else if (valign == "bottom") { + info.y = y - info.height; } - } + }; } $.plot.plugins.push({ diff --git a/jquery.flot.js b/jquery.flot.js index 72f2ebe..8b0000e 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -109,18 +109,6 @@ Licensed under the MIT license. // re-calculating them when the plot is re-rendered in a loop. this._textCache = {}; - - // A 'hot' copy of the text cache; it holds only info that has been - // accessed in the past render cycle. With each render it is saved as - // the new text cache, as an alternative to more complicated ways of - // expiring items that are no longer needed. - - // NOTE: It's unclear how this compares performance-wise to keeping a - // single cache and looping over it to delete expired items. This way - // is certainly less operations, but seems like it might result in more - // garbage collection and possibly increased cache-insert times. - - this._activeTextCache = {}; } // Resizes the canvas to the given dimensions. @@ -171,70 +159,73 @@ Licensed under the MIT license. context.scale(pixelRatio, pixelRatio); }; - // Clears the entire canvas area, including overlaid text. + // Clears the entire canvas area, not including any overlaid HTML text Canvas.prototype.clear = function() { this.context.clearRect(0, 0, this.width, this.height); - if (this.text) { - this.text.html(""); - } }; - // Finishes rendering the canvas, including populating the text overlay. + // Finishes rendering the canvas, including managing the text overlay. Canvas.prototype.render = function() { - var cache = this._activeTextCache; - - // Swap out the text cache for the 'hot cache' that we've been filling - // out since the last call to render. - - this._activeTextCache = {}; - this._textCache = cache; + var cache = this._textCache, + cacheHasText = false, + info, key; // Check whether the cache actually has any entries. - var cacheHasText = false; - - for (var key in cache) { + for (key in cache) { if (hasOwnProperty.call(cache, key)) { cacheHasText = true; break; } } - // Render the contents of the cache + if (!cacheHasText) { + return; + } + + // Create the HTML text layer, if it doesn't already exist. + + if (!this.text) { + this.text = $("
") + .addClass("flot-text") + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + }) + .insertAfter(this.element); + } - if (cacheHasText) { + // Add all the elements to the text layer, then add it to the DOM at + // the end, so we only trigger a single redraw. - // Create the HTML text layer, if it doesn't already exist; if it - // does, detach it so we don't get repaints while adding elements. + this.text.hide(); - if (!this.text) { - this.text = $("
") - .addClass("flot-text") - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0 - }); - } else { - this.text.detach(); - } + for (key in cache) { + if (hasOwnProperty.call(cache, key)) { - // Add all the elements to the text layer, then add it to the DOM - // at the end, so we only trigger a single redraw. + info = cache[key]; - for (var key in cache) { - if (hasOwnProperty.call(cache, key)) { - this.text.append(cache[key].element); + if (info.active) { + if (!info.rendered) { + this.text.append(info.element); + info.rendered = true; + } + } else { + delete cache[key]; + if (info.rendered) { + info.element.detach(); + } } } - - this.text.insertAfter(this.element); } + + this.text.show(); }; // Creates (if necessary) and returns a text info object. @@ -242,11 +233,11 @@ Licensed under the MIT license. // The object looks like this: // // { + // width: Width of the text's wrapper div. + // height: Height of the text's wrapper div. + // 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. - // dimensions: { - // width: Width of the text's wrapper div. - // height: Height of the text's wrapper div. - // } // } // // Canvas maintains a cache of recently-used text info objects; getTextInfo @@ -280,7 +271,7 @@ Licensed under the MIT license. cacheKey = text + "-" + textStyle + "-" + angle; - info = this._textCache[cacheKey] || this._activeTextCache[cacheKey]; + info = this._textCache[cacheKey]; // If we can't find a matching element in our cache, create a new one @@ -294,7 +285,7 @@ Licensed under the MIT license. if (typeof font === "object") { element.css({ font: textStyle, - color: font.color, + color: font.color }); } else if (typeof font === "string") { element.addClass(font); @@ -303,29 +294,25 @@ Licensed under the MIT license. element.appendTo(this.container); info = { + active: false, + rendered: false, element: element, - dimensions: { - width: element.outerWidth(true), - height: element.outerHeight(true) - } + width: element.outerWidth(true), + height: element.outerHeight(true) }; element.detach(); - } - // Save the entry to the 'hot' text cache, marking it as active and - // preserving it for the next render pass. - - this._activeTextCache[cacheKey] = info; + this._textCache[cacheKey] = info; + } return info; }; - // Draws a text string onto the canvas. + // Adds a text string to the canvas text overlay. // - // The text isn't necessarily drawn immediately; some implementations may - // buffer it to improve performance. Text is only guaranteed to be drawn - // after the Canvas render method has been called. + // 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 {number} x X coordinate at which to draw the text. // @param {number} y Y coordinate at which to draw the text. @@ -339,33 +326,64 @@ Licensed under the MIT license. // @param {string=} valign Vertical alignment of the text; either "top", // "middle" or "bottom". - Canvas.prototype.drawText = function(x, y, text, font, angle, halign, valign) { + Canvas.prototype.addText = function(x, y, text, font, angle, halign, valign) { + + var info = this.getTextInfo(text, font, angle); - var info = this.getTextInfo(text, font, angle), - dimensions = info.dimensions; + // Mark the div for inclusion in the next render pass + + info.active = true; // Tweak the div's position to match the text's alignment if (halign == "center") { - x -= dimensions.width / 2; + x -= info.width / 2; } else if (halign == "right") { - x -= dimensions.width; + x -= info.width; } if (valign == "middle") { - y -= dimensions.height / 2; + y -= info.height / 2; } else if (valign == "bottom") { - y -= dimensions.height; + y -= info.height; } // Move the element to its final position within the container info.element.css({ - top: parseInt(y), - left: parseInt(x) + top: parseInt(y, 10), + left: parseInt(x, 10) }); }; + // Removes one or more text strings from the canvas text overlay. + // + // If no parameters are given, all text within the container is removed. + // The text is not actually removed; it is simply marked as inactive, which + // will result in its removal on the next render pass. + // + // @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(text, font, angle) { + if (text == null) { + var cache = this._textCache; + for (var key in cache) { + if (hasOwnProperty.call(cache, key)) { + cache[key].active = false; + } + } + } else { + var info = this.getTextInfo(text, font, angle); + if (info != null) { + info.active = false; + } + } + }; + /////////////////////////////////////////////////////////////////////////// // The top-level container for the entire plot. @@ -1225,18 +1243,17 @@ Licensed under the MIT license. for (var i = 0; i < ticks.length; ++i) { - var t = ticks[i], - dimensions; + var t = ticks[i]; if (!t.label) continue; - dimensions = surface.getTextInfo(t.label, font).dimensions; + var info = surface.getTextInfo(t.label, font); if (opts.labelWidth == null) - axisw = Math.max(axisw, dimensions.width); + axisw = Math.max(axisw, info.width); if (opts.labelHeight == null) - axish = Math.max(axish, dimensions.height); + axish = Math.max(axish, info.height); } axis.labelWidth = Math.ceil(axisw); @@ -1431,6 +1448,10 @@ Licensed under the MIT license. setTransformationHelpers(axis); }); + if (showGrid) { + drawAxisLabels(); + } + insertLegend(); } @@ -1667,7 +1688,6 @@ Licensed under the MIT license. if (grid.show && !grid.aboveData) { drawGrid(); - drawAxisLabels(); } for (var i = 0; i < series.length; ++i) { @@ -1679,7 +1699,6 @@ Licensed under the MIT license. if (grid.show && grid.aboveData) { drawGrid(); - drawAxisLabels(); } surface.render(); @@ -1955,6 +1974,8 @@ Licensed under the MIT license. function drawAxisLabels() { + surface.removeText(); + $.each(allAxes(), function (_, axis) { if (!axis.show || axis.ticks.length == 0) return; @@ -1989,7 +2010,7 @@ Licensed under the MIT license. } } - surface.drawText(x, y, tick.label, font, null, halign, valign); + surface.addText(x, y, tick.label, font, null, halign, valign); } }); } From a036aa962a3b915c583df5cd8669622f0b8ec5f5 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Sun, 24 Feb 2013 09:47:21 -0500 Subject: [PATCH 14/24] Reverse cache key order to ensure uniqueness. Also switch from dashes to pipes, and remove the angle for now, since we don't currently support rotated text. --- jquery.flot.canvas.js | 3 ++- jquery.flot.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/jquery.flot.canvas.js b/jquery.flot.canvas.js index b3367bc..3499d50 100644 --- a/jquery.flot.canvas.js +++ b/jquery.flot.canvas.js @@ -184,8 +184,9 @@ browser, but needs to redraw with canvas text when exporting as an image. // The text + style + angle uniquely identify the text's dimensions // and content; we'll use them to build the entry's text cache key. + // NOTE: We don't support rotated text yet, so the angle is unused. - cacheKey = text + "-" + textStyle + "-" + angle; + cacheKey = textStyle + "|" + text; info = this._textCache[cacheKey]; diff --git a/jquery.flot.js b/jquery.flot.js index 8b0000e..f73cd05 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -268,8 +268,9 @@ Licensed under the MIT license. // The text + style + angle uniquely identify the text's dimensions and // content; we'll use them to build this entry's text cache key. + // NOTE: We don't support rotated text yet, so the angle is unused. - cacheKey = text + "-" + textStyle + "-" + angle; + cacheKey = textStyle + "|" + text; info = this._textCache[cacheKey]; From e7de87352483c0700851662c0951f1dc26b09b27 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Sun, 24 Feb 2013 11:54:58 -0500 Subject: [PATCH 15/24] Factor out text layer creation to its own method. This sets the stage for allowing the use of multiple layers. --- jquery.flot.js | 46 ++++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/jquery.flot.js b/jquery.flot.js index f73cd05..6c0db7b 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -171,7 +171,7 @@ Licensed under the MIT license. var cache = this._textCache, cacheHasText = false, - info, key; + key; // Check whether the cache actually has any entries. @@ -188,23 +188,13 @@ Licensed under the MIT license. // Create the HTML text layer, if it doesn't already exist. - if (!this.text) { - this.text = $("
") - .addClass("flot-text") - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0 - }) - .insertAfter(this.element); - } + var layer = this.getTextLayer(), + info; // Add all the elements to the text layer, then add it to the DOM at // the end, so we only trigger a single redraw. - this.text.hide(); + layer.hide(); for (key in cache) { if (hasOwnProperty.call(cache, key)) { @@ -213,7 +203,7 @@ Licensed under the MIT license. if (info.active) { if (!info.rendered) { - this.text.append(info.element); + layer.append(info.element); info.rendered = true; } } else { @@ -225,7 +215,31 @@ Licensed under the MIT license. } } - this.text.show(); + layer.show(); + }; + + // Creates (if necessary) and returns the text overlay container. + // + // @return {object} The jQuery-wrapped text-layer div. + + Canvas.prototype.getTextLayer = function() { + + // Create the text layer if it doesn't exist + + if (!this.text) { + this.text = $("
") + .addClass("flot-text") + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + }) + .insertAfter(this.element); + } + + return this.text; }; // Creates (if necessary) and returns a text info object. From 4203a66eba482027b3e92c56aff44ce69aa9dfb4 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Sun, 24 Feb 2013 12:13:39 -0500 Subject: [PATCH 16/24] Add text to its actual layer before measuring it. The getTextInfo method previously added new text to the top-level container when measuring it. Now it adds the text to the text layer, just as it will be when rendered, so that parent-child CSS rules can resolve correctly. This also avoids having to safe a reference to the top-level container, since it wasn't used anywhere else. --- examples/basic-canvas.html | 39 ++++++++++++++++++++++++++++++++++++++ jquery.flot.canvas.js | 6 +++++- jquery.flot.js | 14 ++++++-------- 3 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 examples/basic-canvas.html diff --git a/examples/basic-canvas.html b/examples/basic-canvas.html new file mode 100644 index 0000000..d9610eb --- /dev/null +++ b/examples/basic-canvas.html @@ -0,0 +1,39 @@ + + + + + Flot Examples + + + + + + + +

Flot Examples

+ +
+ +

Simple example. You don't need to specify much to get an + attractive look. Put in a placeholder, make sure you set its + dimensions (otherwise the plot library will barf) and call the + plot function with the data. The axes are automatically + scaled.

+ + + + + diff --git a/jquery.flot.canvas.js b/jquery.flot.canvas.js index 3499d50..81e0b3e 100644 --- a/jquery.flot.canvas.js +++ b/jquery.flot.canvas.js @@ -201,7 +201,11 @@ browser, but needs to redraw with canvas text when exporting as an image. var element = $("
").html(text) .addClass(typeof font === "string" ? font : null) - .appendTo(this.container); + .css({ + position: "absolute", + top: -9999 + }) + .appendTo(this.getTextLayer()); font = { style: element.css("font-style"), diff --git a/jquery.flot.js b/jquery.flot.js index 6c0db7b..6823e3c 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -56,8 +56,6 @@ Licensed under the MIT license. function Canvas(cls, container) { - this.container = container; - var element = document.createElement("canvas"); element.className = cls; this.element = element; @@ -292,10 +290,12 @@ Licensed under the MIT license. if (info == null) { - var element = $("
").html(text).css({ - position: "absolute", - top: -9999 - }); + var element = $("
").html(text) + .css({ + position: "absolute", + top: -9999 + }) + .appendTo(this.getTextLayer()); if (typeof font === "object") { element.css({ @@ -306,8 +306,6 @@ Licensed under the MIT license. element.addClass(font); } - element.appendTo(this.container); - info = { active: false, rendered: false, From 77e50b175da09a092449c50314b45d7d369cb9b9 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Sun, 24 Feb 2013 14:38:09 -0500 Subject: [PATCH 17/24] Allow text to be divided between multiple layers. This lets users 'namespace' text more naturally, i.e. placing x-axis labels in a different container from y-axis labels, providing more flexibility when it comes to styling and interactivity. Internally the text cache now has a second tier: layers > text > info. --- jquery.flot.canvas.js | 140 +++++++++++++++++++++-------------------- jquery.flot.js | 141 ++++++++++++++++++++++-------------------- 2 files changed, 142 insertions(+), 139 deletions(-) diff --git a/jquery.flot.canvas.js b/jquery.flot.canvas.js index 81e0b3e..f4666c1 100644 --- a/jquery.flot.canvas.js +++ b/jquery.flot.canvas.js @@ -53,87 +53,79 @@ browser, but needs to redraw with canvas text when exporting as an image. } var context = this.context, - cache = this._textCache, - cacheHasText = false, - key; + cache = this._textCache; - // Check whether the cache actually has any entries. + // For each text layer, render elements marked as active - for (key in cache) { - if (hasOwnProperty.call(cache, key)) { - cacheHasText = true; - break; - } - } + context.save(); - if (!cacheHasText) { - return; - } + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { - // Render the contents of the cache + var layerCache = cache[layerKey]; - context.save(); + for (var key in layerCache) { + if (hasOwnProperty.call(layerCache, key)) { - for (key in cache) { - if (hasOwnProperty.call(cache, key)) { + var info = layerCache[key]; - var info = cache[key]; + if (!info.active) { + delete cache[key]; + continue; + } - if (!info.active) { - delete cache[key]; - continue; - } + var x = info.x, + y = info.y, + lines = info.lines, + halign = info.halign; - var x = info.x, - y = info.y, - lines = info.lines, - halign = info.halign; + context.fillStyle = info.font.color; + context.font = info.font.definition; - context.fillStyle = info.font.color; - context.font = info.font.definition; + // TODO: Comments in Ole's implementation indicate that + // some browsers differ in their interpretation of 'top'; + // so far I don't see this, but it requires more testing. + // We'll stick with top until this can be verified. - // TODO: Comments in Ole's implementation indicate that - // some browsers differ in their interpretation of 'top'; - // so far I don't see this, but it requires more testing. - // We'll stick with top until this can be verified. + // Original comment was: + // Top alignment 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. - // Original comment was: - // Top alignment 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. + context.textBaseline = "top"; - context.textBaseline = "top"; + for (var i = 0; i < lines.length; ++i) { - for (var i = 0; i < lines.length; ++i) { + var line = lines[i], + linex = x; - var line = lines[i], - linex = x; + // Apply horizontal alignment per-line - // Apply horizontal alignment per-line + if (halign == "center") { + linex -= line.width / 2; + } else if (halign == "right") { + linex -= line.width; + } - if (halign == "center") { - linex -= line.width / 2; - } else if (halign == "right") { - linex -= line.width; - } + // FIXME: LEGACY BROWSER FIX + // AFFECTS: Opera < 12.00 - // FIXME: LEGACY BROWSER FIX - // AFFECTS: Opera < 12.00 + // Round the coordinates, since Opera otherwise + // switches to uglier (probably non-hinted) rendering. + // Also offset the y coordinate, since Opera is off + // pretty consistently compared to the other browsers. - // Round the coordinates, since Opera otherwise - // switches to uglier (probably non-hinted) rendering. - // Also offset the y coordinate, since Opera is off - // pretty consistently compared to the other browsers. + if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { + linex = Math.floor(linex); + y = Math.ceil(y - 2); + } - if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { - linex = Math.floor(linex); - y = Math.ceil(y - 2); + context.fillText(line.text, linex, y); + y += line.height; + } } - - context.fillText(line.text, linex, y); - y += line.height; } } } @@ -162,13 +154,13 @@ browser, but needs to redraw with canvas text when exporting as an image. // }, // } - Canvas.prototype.getTextInfo = function(text, font, angle) { + Canvas.prototype.getTextInfo = function(layer, text, font, angle) { if (!plot.getOptions().canvas) { - return getTextInfo.call(this, text, font, angle); + return getTextInfo.call(this, layer, text, font, angle); } - var textStyle, cacheKey, info; + var textStyle, cache, cacheKey, info; // Cast the value to a string, in case we were given a number @@ -182,13 +174,21 @@ browser, but needs to redraw with canvas text when exporting as an image. textStyle = font; } + // Retrieve (or create) the cache for the text's layer + + cache = this._textCache[layer]; + + if (cache == null) { + cache = this._textCache[layer] = {}; + } + // The text + style + angle uniquely identify the text's dimensions // and content; we'll use them to build the entry's text cache key. // NOTE: We don't support rotated text yet, so the angle is unused. cacheKey = textStyle + "|" + text; - info = this._textCache[cacheKey]; + info = cache[cacheKey]; if (info == null) { @@ -205,7 +205,7 @@ browser, but needs to redraw with canvas text when exporting as an image. position: "absolute", top: -9999 }) - .appendTo(this.getTextLayer()); + .appendTo(this.getTextLayer(layer)); font = { style: element.css("font-style"), @@ -224,7 +224,7 @@ browser, but needs to redraw with canvas text when exporting as an image. // Create a new info object, initializing the dimensions to // zero so we can count them up line-by-line. - info = { + info = cache[cacheKey] = { x: null, y: null, width: 0, @@ -275,8 +275,6 @@ browser, but needs to redraw with canvas text when exporting as an image. }); } - this._textCache[cacheKey] = info; - context.restore(); } @@ -285,13 +283,13 @@ browser, but needs to redraw with canvas text when exporting as an image. // Adds a text string to the canvas text overlay. - Canvas.prototype.addText = function(x, y, text, font, angle, halign, valign) { + Canvas.prototype.addText = function(layer, x, y, text, font, angle, halign, valign) { if (!plot.getOptions().canvas) { - return addText.call(this, x, y, text, font, angle, halign, valign); + return addText.call(this, layer, x, y, text, font, angle, halign, valign); } - var info = this.getTextInfo(text, font, angle); + var info = this.getTextInfo(layer, text, font, angle); info.x = x; info.y = y; diff --git a/jquery.flot.js b/jquery.flot.js index 6823e3c..c03d7f9 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -99,9 +99,9 @@ Licensed under the MIT license. this.resize(container.width(), container.height()); - // Container for HTML text overlaid onto the canvas; created on demand + // Collection of HTML div layers for text overlaid onto the canvas - this.text = 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. @@ -167,66 +167,58 @@ Licensed under the MIT license. Canvas.prototype.render = function() { - var cache = this._textCache, - cacheHasText = false, - key; + var cache = this._textCache; - // Check whether the cache actually has any entries. + // For each text layer, add elements marked as active that haven't + // already been rendered, and remove those that are no longer active. - for (key in cache) { - if (hasOwnProperty.call(cache, key)) { - cacheHasText = true; - break; - } - } - - if (!cacheHasText) { - return; - } - - // Create the HTML text layer, if it doesn't already exist. + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { - var layer = this.getTextLayer(), - info; + var layer = this.getTextLayer(layerKey), + layerCache = cache[layerKey]; - // Add all the elements to the text layer, then add it to the DOM at - // the end, so we only trigger a single redraw. + layer.hide(); - layer.hide(); + for (var key in layerCache) { + if (hasOwnProperty.call(layerCache, key)) { - for (key in cache) { - if (hasOwnProperty.call(cache, key)) { + var info = layerCache[key]; - info = cache[key]; - - if (info.active) { - if (!info.rendered) { - layer.append(info.element); - info.rendered = true; - } - } else { - delete cache[key]; - if (info.rendered) { - info.element.detach(); + if (info.active) { + if (!info.rendered) { + layer.append(info.element); + info.rendered = true; + } + } else { + delete layerCache[key]; + if (info.rendered) { + info.element.detach(); + } + } } } + + layer.show(); } } - - 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() { + Canvas.prototype.getTextLayer = function(classes) { + + var layer = this.text[classes]; // Create the text layer if it doesn't exist - if (!this.text) { - this.text = $("
") - .addClass("flot-text") + if (layer == null) { + layer = this.text[classes] = $("
") + .addClass("flot-text " + classes) .css({ position: "absolute", top: 0, @@ -237,7 +229,7 @@ Licensed under the MIT license. .insertAfter(this.element); } - return this.text; + return layer; }; // Creates (if necessary) and returns a text info object. @@ -255,6 +247,8 @@ Licensed under the MIT license. // 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. @@ -262,9 +256,9 @@ Licensed under the MIT license. // Angle is currently unused, it will be implemented in the future. // @return {object} a text info object. - Canvas.prototype.getTextInfo = function(text, font, angle) { + Canvas.prototype.getTextInfo = function(layer, text, font, angle) { - var textStyle, cacheKey, info; + var textStyle, cache, cacheKey, info; // Cast the value to a string, in case we were given a number or such @@ -278,13 +272,21 @@ Licensed under the MIT license. textStyle = font; } + // Retrieve (or create) the cache for the text's layer + + cache = this._textCache[layer]; + + if (cache == null) { + cache = this._textCache[layer] = {}; + } + // The text + style + angle uniquely identify the text's dimensions and // content; we'll use them to build this entry's text cache key. // NOTE: We don't support rotated text yet, so the angle is unused. cacheKey = textStyle + "|" + text; - info = this._textCache[cacheKey]; + info = cache[cacheKey]; // If we can't find a matching element in our cache, create a new one @@ -295,7 +297,7 @@ Licensed under the MIT license. position: "absolute", top: -9999 }) - .appendTo(this.getTextLayer()); + .appendTo(this.getTextLayer(layer)); if (typeof font === "object") { element.css({ @@ -306,7 +308,7 @@ Licensed under the MIT license. element.addClass(font); } - info = { + info = cache[cacheKey] = { active: false, rendered: false, element: element, @@ -315,8 +317,6 @@ Licensed under the MIT license. }; element.detach(); - - this._textCache[cacheKey] = info; } return info; @@ -327,6 +327,8 @@ Licensed under the MIT license. // 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. @@ -339,9 +341,9 @@ Licensed under the MIT license. // @param {string=} valign Vertical alignment of the text; either "top", // "middle" or "bottom". - Canvas.prototype.addText = function(x, y, text, font, angle, halign, valign) { + Canvas.prototype.addText = function(layer, x, y, text, font, angle, halign, valign) { - var info = this.getTextInfo(text, font, angle); + var info = this.getTextInfo(layer, text, font, angle); // Mark the div for inclusion in the next render pass @@ -371,29 +373,30 @@ Licensed under the MIT license. // Removes one or more text strings from the canvas text overlay. // - // If no parameters are given, all text within the container is removed. + // If no parameters are given, all text within the layer is removed. // The text is not actually removed; it is simply marked as inactive, which // will result in its removal on the next render pass. // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this 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(text, font, angle) { + Canvas.prototype.removeText = function(layer, text, font, angle) { if (text == null) { - var cache = this._textCache; - for (var key in cache) { - if (hasOwnProperty.call(cache, key)) { - cache[key].active = false; + var cache = this._textCache[layer]; + if (cache != null) { + for (var key in cache) { + if (hasOwnProperty.call(cache, key)) { + cache[key].active = false; + } } } } else { - var info = this.getTextInfo(text, font, angle); - if (info != null) { - info.active = false; - } + this.getTextInfo(layer, text, font, angle).active = false; } }; @@ -1252,7 +1255,8 @@ Licensed under the MIT license. var opts = axis.options, ticks = axis.ticks || [], axisw = opts.labelWidth || 0, axish = opts.labelHeight || 0, - font = opts.font || "flot-tick-label flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis"; + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis", + font = opts.font || "flot-tick-label"; for (var i = 0; i < ticks.length; ++i) { @@ -1261,7 +1265,7 @@ Licensed under the MIT license. if (!t.label) continue; - var info = surface.getTextInfo(t.label, font); + var info = surface.getTextInfo(layer, t.label, font); if (opts.labelWidth == null) axisw = Math.max(axisw, info.width); @@ -1987,16 +1991,17 @@ Licensed under the MIT license. function drawAxisLabels() { - surface.removeText(); - $.each(allAxes(), function (_, axis) { if (!axis.show || axis.ticks.length == 0) return; var box = axis.box, - font = axis.options.font || "flot-tick-label flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis", + font = axis.options.font || "flot-tick-label", tick, x, y, halign, valign; + surface.removeText(layer); + for (var i = 0; i < axis.ticks.length; ++i) { tick = axis.ticks[i]; @@ -2023,7 +2028,7 @@ Licensed under the MIT license. } } - surface.addText(x, y, tick.label, font, null, halign, valign); + surface.addText(layer, x, y, tick.label, font, null, halign, valign); } }); } From a2dd0645aa281950ae49b5d191e96a65db5071a6 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Sun, 24 Feb 2013 18:08:24 -0500 Subject: [PATCH 18/24] Break text styles into their own cache tier. Previously the cache was divided only by layer, with entries keyed on a string built from the text and style. Now the style has its own tier in the cache, i.e. layers > styles > text > info. This introduces some complexity, since the nested for loops are ugly, but at the same time we avoid having to create the cache-key strings. More importantly it solves the problem of uniqueness that exists when we try to join strings that may contain arbitrary text. It also allows a further optimization in the canvas plugin, which can now set text style and color just once per distinct style, instead of with every string. --- jquery.flot.canvas.js | 155 ++++++++++++++++++++++-------------------- jquery.flot.js | 68 ++++++++++-------- 2 files changed, 121 insertions(+), 102 deletions(-) diff --git a/jquery.flot.canvas.js b/jquery.flot.canvas.js index f4666c1..d4cf564 100644 --- a/jquery.flot.canvas.js +++ b/jquery.flot.canvas.js @@ -61,69 +61,80 @@ browser, but needs to redraw with canvas text when exporting as an image. for (var layerKey in cache) { if (hasOwnProperty.call(cache, layerKey)) { - var layerCache = cache[layerKey]; - - for (var key in layerCache) { - if (hasOwnProperty.call(layerCache, key)) { - - var info = layerCache[key]; - - if (!info.active) { - delete cache[key]; - continue; - } - - var x = info.x, - y = info.y, - lines = info.lines, - halign = info.halign; - - context.fillStyle = info.font.color; - context.font = info.font.definition; - - // TODO: Comments in Ole's implementation indicate that - // some browsers differ in their interpretation of 'top'; - // so far I don't see this, but it requires more testing. - // We'll stick with top until this can be verified. - - // Original comment was: - // Top alignment 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. - - context.textBaseline = "top"; - - for (var i = 0; i < lines.length; ++i) { - - var line = lines[i], - linex = x; - - // Apply horizontal alignment per-line - - if (halign == "center") { - linex -= line.width / 2; - } else if (halign == "right") { - linex -= line.width; - } - - // FIXME: LEGACY BROWSER FIX - // AFFECTS: Opera < 12.00 - - // Round the coordinates, since Opera otherwise - // switches to uglier (probably non-hinted) rendering. - // Also offset the y coordinate, since Opera is off - // pretty consistently compared to the other browsers. - - if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { - linex = Math.floor(linex); - y = Math.ceil(y - 2); + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey], + updateStyles = true; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + + var info = styleCache[key]; + + if (!info.active) { + delete styleCache[key]; + continue; + } + + var x = info.x, + y = info.y, + lines = info.lines, + halign = info.halign; + + // Since every element at this level of the cache have the + // same font and fill styles, we can just change them once + // using the values from the first element. + + if (updateStyles) { + context.fillStyle = info.font.color; + context.font = info.font.definition; + updateStyles = false; + } + + // TODO: Comments in Ole's implementation indicate that + // some browsers differ in their interpretation of 'top'; + // so far I don't see this, but it requires more testing. + // We'll stick with top until this can be verified. + + // Original comment was: + // Top alignment 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. + + context.textBaseline = "top"; + + for (var i = 0; i < lines.length; ++i) { + + var line = lines[i], + linex = x; + + // Apply horizontal alignment per-line + + if (halign == "center") { + linex -= line.width / 2; + } else if (halign == "right") { + linex -= line.width; + } + + // FIXME: LEGACY BROWSER FIX + // AFFECTS: Opera < 12.00 + + // Round the coordinates, since Opera otherwise + // switches to uglier (probably non-hinted) rendering. + // Also offset the y coordinate, since Opera is off + // pretty consistently compared to the other browsers. + + if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { + linex = Math.floor(linex); + y = Math.ceil(y - 2); + } + + context.fillText(line.text, linex, y); + y += line.height; + } } - - context.fillText(line.text, linex, y); - y += line.height; } } } @@ -160,7 +171,7 @@ browser, but needs to redraw with canvas text when exporting as an image. return getTextInfo.call(this, layer, text, font, angle); } - var textStyle, cache, cacheKey, info; + var textStyle, layerCache, styleCache, info; // Cast the value to a string, in case we were given a number @@ -174,21 +185,21 @@ browser, but needs to redraw with canvas text when exporting as an image. textStyle = font; } - // Retrieve (or create) the cache for the text's layer + // Retrieve (or create) the cache for the text's layer and styles - cache = this._textCache[layer]; + layerCache = this._textCache[layer]; - if (cache == null) { - cache = this._textCache[layer] = {}; + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; } - // The text + style + angle uniquely identify the text's dimensions - // and content; we'll use them to build the entry's text cache key. - // NOTE: We don't support rotated text yet, so the angle is unused. + styleCache = layerCache[textStyle]; - cacheKey = textStyle + "|" + text; + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } - info = cache[cacheKey]; + info = styleCache[text]; if (info == null) { @@ -224,7 +235,7 @@ browser, but needs to redraw with canvas text when exporting as an image. // Create a new info object, initializing the dimensions to // zero so we can count them up line-by-line. - info = cache[cacheKey] = { + info = styleCache[text] = { x: null, y: null, width: 0, diff --git a/jquery.flot.js b/jquery.flot.js index c03d7f9..9f6a909 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -180,20 +180,23 @@ Licensed under the MIT license. layer.hide(); - for (var key in layerCache) { - if (hasOwnProperty.call(layerCache, key)) { - - var info = layerCache[key]; - - if (info.active) { - if (!info.rendered) { - layer.append(info.element); - info.rendered = true; - } - } else { - delete layerCache[key]; - if (info.rendered) { - info.element.detach(); + 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 info = styleCache[key]; + if (info.active) { + if (!info.rendered) { + layer.append(info.element); + info.rendered = true; + } + } else { + delete styleCache[key]; + if (info.rendered) { + info.element.detach(); + } + } } } } @@ -258,7 +261,7 @@ Licensed under the MIT license. Canvas.prototype.getTextInfo = function(layer, text, font, angle) { - var textStyle, cache, cacheKey, info; + var textStyle, layerCache, styleCache, info; // Cast the value to a string, in case we were given a number or such @@ -272,21 +275,21 @@ Licensed under the MIT license. textStyle = font; } - // Retrieve (or create) the cache for the text's layer + // Retrieve (or create) the cache for the text's layer and styles - cache = this._textCache[layer]; + layerCache = this._textCache[layer]; - if (cache == null) { - cache = this._textCache[layer] = {}; + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; } - // The text + style + angle uniquely identify the text's dimensions and - // content; we'll use them to build this entry's text cache key. - // NOTE: We don't support rotated text yet, so the angle is unused. + styleCache = layerCache[textStyle]; - cacheKey = textStyle + "|" + text; + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } - info = cache[cacheKey]; + info = styleCache[text]; // If we can't find a matching element in our cache, create a new one @@ -308,7 +311,7 @@ Licensed under the MIT license. element.addClass(font); } - info = cache[cacheKey] = { + info = styleCache[text] = { active: false, rendered: false, element: element, @@ -387,11 +390,16 @@ Licensed under the MIT license. Canvas.prototype.removeText = function(layer, text, font, angle) { if (text == null) { - var cache = this._textCache[layer]; - if (cache != null) { - for (var key in cache) { - if (hasOwnProperty.call(cache, key)) { - cache[key].active = false; + 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)) { + styleCache[key].active = false; + } + } } } } From e354071741e750e5ae15e20b309df604338a7426 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Sun, 3 Mar 2013 17:29:06 -0500 Subject: [PATCH 19/24] Minor cleanup of text-style color defaults. --- jquery.flot.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/jquery.flot.js b/jquery.flot.js index 9f6a909..14025b5 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -34,16 +34,20 @@ Licensed under the MIT license. // the actual Flot code (function($) { + // Cache the prototype hasOwnProperty for faster access + + var hasOwnProperty = Object.prototype.hasOwnProperty; + // Add default styles for tick labels and other text + var STYLES = [ + ".flot-tick-label {font-size:smaller;color:#545454;}" + ]; + $(function() { - $("head").prepend(""); + $("head").prepend(""); }); - // Cache the prototype hasOwnProperty for faster access - - var hasOwnProperty = Object.prototype.hasOwnProperty; - /////////////////////////////////////////////////////////////////////////// // The Canvas object is a wrapper around an HTML5 tag. // @@ -668,6 +672,9 @@ Licensed under the MIT license. options.xaxes[i] = axisOptions; if (axisOptions.font) { axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } } } @@ -677,6 +684,9 @@ Licensed under the MIT license. options.yaxes[i] = axisOptions; if (axisOptions.font) { axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } } } From bb0acac9c7d46179982ae44dda400b252bcc062d Mon Sep 17 00:00:00 2001 From: David Schnur Date: Sun, 3 Mar 2013 17:31:26 -0500 Subject: [PATCH 20/24] Give tick labels the 'tickLabel' class. The tickLabel class is deprecated in favor of flot-tick-label, but we'll continue to use it until the release of version 1.0.0, for backwards-compatibility. --- jquery.flot.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jquery.flot.js b/jquery.flot.js index 14025b5..4dbd4d8 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -1274,7 +1274,7 @@ Licensed under the MIT license. var opts = axis.options, ticks = axis.ticks || [], axisw = opts.labelWidth || 0, axish = opts.labelHeight || 0, layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis", - font = opts.font || "flot-tick-label"; + font = opts.font || "flot-tick-label tickLabel"; for (var i = 0; i < ticks.length; ++i) { @@ -2015,7 +2015,7 @@ Licensed under the MIT license. var box = axis.box, layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis", - font = axis.options.font || "flot-tick-label", + font = axis.options.font || "flot-tick-label tickLabel", tick, x, y, halign, valign; surface.removeText(layer); From 27c701112d9bbbd99efa495668a3414414bdc0d6 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Sun, 3 Mar 2013 17:32:29 -0500 Subject: [PATCH 21/24] Updated the API docs for axis text changes. --- API.md | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/API.md b/API.md index bcf5d25..c474fd8 100644 --- a/API.md +++ b/API.md @@ -266,11 +266,27 @@ you can also set the color of the ticks separately with "tickColor" (otherwise it's autogenerated as the base color with some transparency). -You can customize the font used to draw the labels with CSS or -directly with "font". The default value of null means that the font is -read from the font style on the placeholder element (80% the size of -that to be precise). If you set it directly with "font: { ... }", the -format is like this: +You can customize the font used to draw the labels with CSS or directly via the +"font" option. When "font" is null - the default - each tick label is given the +'flot-tick-label' class. For compatibility with Flot 0.7 and earlier the labels +are also given the 'tickLabel' class, but this is deprecated and scheduled to +be removed with the release of version 1.0.0. + +To enable more granular control over styles, labels are divided between a set +of text containers, with each holding the labels for one axis. These containers +are given the classes 'flot-text', 'flot-[x|y]-axis', and 'flot-[x|y]#-axis', +where '#' is the number of the axis when there are multiple axes. For example, +the x-axis labels for a simple plot with only one x-axis might look like this: + +```html +
+
January 2013
+ ... +
+``` + +For direct control over label styles you can also provide "font" as an object +with this format: ```js { @@ -278,7 +294,8 @@ format is like this: style: "italic", weight: "bold", family: "sans-serif", - variant: "small-caps" + variant: "small-caps", + color: "#545454" } ``` From 0df6bc4a66234303bb3ff5f3752fdd5ef100f755 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Sun, 3 Mar 2013 18:03:39 -0500 Subject: [PATCH 22/24] Add back legacy styles for tick label containers. These styles are deprecated, but we'll continue to use them until the release of version 1.0.0, for backwards-compatibility. --- jquery.flot.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/jquery.flot.js b/jquery.flot.js index 4dbd4d8..b337149 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -1273,7 +1273,8 @@ Licensed under the MIT license. var opts = axis.options, ticks = axis.ticks || [], axisw = opts.labelWidth || 0, axish = opts.labelHeight || 0, - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis", + 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) { @@ -2014,11 +2015,12 @@ Licensed under the MIT license. return; var box = axis.box, - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis", + 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; - surface.removeText(layer); + surface.removeText(layer); for (var i = 0; i < axis.ticks.length; ++i) { From 7a799baeb830f58ea530a668a4c7c7f9a714015a Mon Sep 17 00:00:00 2001 From: David Schnur Date: Sun, 3 Mar 2013 18:03:52 -0500 Subject: [PATCH 23/24] Updated NEWS for axis text changes. --- NEWS.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/NEWS.md b/NEWS.md index ddae80f..48596c9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -17,12 +17,26 @@ standard strftime specifiers, plus one nonstandard specifier for quarters. Additionally, if a strftime function is found in the Date object's prototype, it will be used instead of the built-in formatter. -Axis labels are now drawn with canvas text with some parsing to support -newlines. This solves various issues but also means that they no longer -support HTML markup, can be accessed as DOM elements or styled directly with -CSS. Some older browsers lack this function of the canvas API (this doesn't -affect IE); if this is a problem, either continue using an older version of -Flot or try an emulation helper such as canvas-text or Flashcanvas. +Axis tick labels now use the class 'flot-tick-label' instead of 'tickLabel'. +The text containers for each axis now use the classes 'flot-[x|y]-axis' and +'flot-[x|y]#-axis' instead of '[x|y]Axis' and '[x|y]#Axis'. For compatibility +with Flot 0.7 and earlier text will continue to use the old classes as well, +but they are considered deprecated and will be removed in a future version. + +A new plugin, jquery.flot.canvas.js, allows axis tick labels to be rendered +directly to the canvas, rather than using HTML elements. This feature can be +toggled with a simple option, making it easy to create interactive plots in the +browser using HTML, then re-render them to canvas for export as an image. + +The plugin tries to remain as faithful as possible to the original HTML render, +and goes so far as to automatically extract styles from CSS, to avoid having to +provide a separate set of styles when rendering to canvas. Due to limitations +of the canvas text API, the plugin cannot reproduce certain features, including +HTML markup embedded in labels, and advanced text styles such as 'em' units. + +The plugin requires support for canvas text, which may not be present in some +older browsers, even if they support the canvas tag itself. To use the plugin +with these browsers try using a shim such as canvas-text or FlashCanvas. The base and overlay canvas are now using the CSS classes "flot-base" and "flot-overlay" to prevent accidental clashes (issue 540). From 39698d3846520398c3d21a109456709211555fe8 Mon Sep 17 00:00:00 2001 From: David Schnur Date: Sun, 3 Mar 2013 18:10:27 -0500 Subject: [PATCH 24/24] Updated credits for canvas text support. --- NEWS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 48596c9..0a68d40 100644 --- a/NEWS.md +++ b/NEWS.md @@ -57,7 +57,8 @@ The base and overlay canvas are now using the CSS classes "flot-base" and - Display time series in different time zones. (patch by Knut Forkalsrud, issue 141) - - Canvas text support for labels. (sponsored by YCharts.com) + - Added a canvas plugin to enable rendering axis tick labels to the canvas. + (sponsored by YCharts.com, implementation by Ole Laursen and David Schnur) - Support for setting the interval between redraws of the overlay canvas with redrawOverlayInterval. (suggested in issue 185)