diff --git a/API.md b/API.md index df0cf24..edf77b4 100644 --- a/API.md +++ b/API.md @@ -275,11 +275,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 { @@ -287,7 +303,8 @@ format is like this: style: "italic", weight: "bold", family: "sans-serif", - variant: "small-caps" + variant: "small-caps", + color: "#545454" } ``` diff --git a/NEWS.md b/NEWS.md index 48c853d..2d33836 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). @@ -43,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) 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 new file mode 100644 index 0000000..d4cf564 --- /dev/null +++ b/jquery.flot.canvas.js @@ -0,0 +1,333 @@ +/* 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 +} + +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 + }; + + // Cache the prototype hasOwnProperty for faster access + + var hasOwnProperty = Object.prototype.hasOwnProperty; + + function init(plot, classes) { + + var Canvas = classes.Canvas, + getTextInfo = Canvas.prototype.getTextInfo, + addText = Canvas.prototype.addText, + render = Canvas.prototype.render; + + // Finishes rendering the canvas, including overlaid text + + Canvas.prototype.render = function() { + + if (!plot.getOptions().canvas) { + return render.call(this); + } + + var context = this.context, + cache = this._textCache; + + // For each text layer, render elements marked as active + + context.save(); + + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { + var layerCache = cache[layerKey]; + 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.restore(); + }; + + // 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. + // }, + // } + + Canvas.prototype.getTextInfo = function(layer, text, font, angle) { + + if (!plot.getOptions().canvas) { + return getTextInfo.call(this, layer, text, font, angle); + } + + var textStyle, layerCache, styleCache, 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; + } + + // Retrieve (or create) the cache for the text's layer and styles + + layerCache = this._textCache[layer]; + + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; + } + + styleCache = layerCache[textStyle]; + + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } + + info = styleCache[text]; + + if (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 = $("
").html(text) + .addClass(typeof font === "string" ? font : null) + .css({ + position: "absolute", + top: -9999 + }) + .appendTo(this.getTextLayer(layer)); + + 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 = styleCache[text] = { + x: null, + y: null, + width: 0, + height: 0, + active: false, + lines: [], + font: { + definition: textStyle, + color: font.color + } + }; + + 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.width = Math.max(lineWidth, info.width); + info.height += lineHeight; + + info.lines.push({ + text: lineText, + width: lineWidth, + height: lineHeight + }); + } + + context.restore(); + } + + return info; + }; + + // Adds a text string to the canvas text overlay. + + Canvas.prototype.addText = function(layer, x, y, text, font, angle, halign, valign) { + + if (!plot.getOptions().canvas) { + return addText.call(this, layer, x, y, text, font, angle, halign, valign); + } + + var info = this.getTextInfo(layer, 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({ + init: init, + options: options, + name: "canvas", + version: "1.0" + }); + +})(jQuery); diff --git a/jquery.flot.js b/jquery.flot.js index c41b531..661007a 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -33,6 +33,388 @@ 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(""); + }); + + /////////////////////////////////////////////////////////////////////////// + // 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; + this.element = element; + + $(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.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()); + + // Collection of HTML div layers for text overlaid onto the canvas + + this.text = {}; + + // Cache of text fragments and metrics, so we can avoid expensively + // re-calculating them when the plot is re-rendered in a loop. + + this._textCache = {}; + } + + // Resizes the canvas to the given dimensions. + // + // @param {number} width New width of the canvas, in pixels. + // @param {number} width New height of the canvas, in pixels. + + Canvas.prototype.resize = function(width, height) { + + if (width <= 0 || height <= 0) { + throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); + } + + var element = this.element, + context = this.context, + pixelRatio = this.pixelRatio; + + // Resize the canvas, increasing its density based on the display's + // pixel ratio; basically giving it more pixels without increasing the + // size of its element, to take advantage of the fact that retina + // displays have that many more pixels in the same advertised space. + + // Resizing should reset the state (excanvas seems to be buggy though) + + if (this.width != width) { + element.width = width * pixelRatio; + element.style.width = width + "px"; + this.width = width; + } + + if (this.height != height) { + element.height = height * pixelRatio; + element.style.height = height + "px"; + this.height = height; + } + + // Save the context, so we can reset in case we get replotted. The + // restore ensure that we're really back at the initial state, and + // should be safe even if we haven't saved the initial state yet. + + context.restore(); + context.save(); + + // Scale the coordinate space to match the display density; so even though we + // may have twice as many pixels, we still want lines and other drawing to + // appear at the same size; the extra pixels will just make them crisper. + + context.scale(pixelRatio, pixelRatio); + }; + + // Clears the entire canvas area, not including any overlaid HTML text + + Canvas.prototype.clear = function() { + this.context.clearRect(0, 0, this.width, this.height); + }; + + // Finishes rendering the canvas, including managing the text overlay. + + Canvas.prototype.render = function() { + + var cache = this._textCache; + + // For each text layer, add elements marked as active that haven't + // already been rendered, and remove those that are no longer active. + + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { + + var layer = this.getTextLayer(layerKey), + layerCache = cache[layerKey]; + + layer.hide(); + + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + var 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(); + } + } + } + } + } + } + + layer.show(); + } + } + }; + + // Creates (if necessary) and returns the text overlay container. + // + // @param {string} classes String of space-separated CSS classes used to + // uniquely identify the text layer. + // @return {object} The jQuery-wrapped text-layer div. + + Canvas.prototype.getTextLayer = function(classes) { + + var layer = this.text[classes]; + + // Create the text layer if it doesn't exist + + if (layer == null) { + layer = this.text[classes] = $("
") + .addClass("flot-text " + classes) + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + }) + .insertAfter(this.element); + } + + return layer; + }; + + // Creates (if necessary) and returns a text info object. + // + // The object looks like this: + // + // { + // width: Width of the text's wrapper div. + // height: Height of the text's wrapper div. + // 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. + // } + // + // Canvas maintains a cache of recently-used text info objects; getTextInfo + // either returns the cached element or creates a new entry. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {string} text Text string to retrieve info for. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @return {object} a text info object. + + Canvas.prototype.getTextInfo = function(layer, text, font, angle) { + + var textStyle, layerCache, styleCache, info; + + // Cast the value to a string, in case we were given a number or such + + text = "" + text; + + // If the font is a font-spec object, generate a CSS font definition + + if (typeof font === "object") { + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; + } else { + textStyle = font; + } + + // Retrieve (or create) the cache for the text's layer and styles + + layerCache = this._textCache[layer]; + + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; + } + + styleCache = layerCache[textStyle]; + + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } + + info = styleCache[text]; + + // If we can't find a matching element in our cache, create a new one + + if (info == null) { + + var element = $("
").html(text) + .css({ + position: "absolute", + top: -9999 + }) + .appendTo(this.getTextLayer(layer)); + + if (typeof font === "object") { + element.css({ + font: textStyle, + color: font.color + }); + } else if (typeof font === "string") { + element.addClass(font); + } + + info = styleCache[text] = { + active: false, + rendered: false, + element: element, + width: element.outerWidth(true), + height: element.outerHeight(true) + }; + + element.detach(); + } + + return info; + }; + + // Adds a text string to the canvas text overlay. + // + // The text isn't drawn immediately; it is marked as rendering, which will + // result in its addition to the canvas on the next render pass. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number} x X coordinate at which to draw the text. + // @param {number} y Y coordinate at which to draw the text. + // @param {string} text Text string to draw. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {string=} halign Horizontal alignment of the text; either "left", + // "center" or "right". + // @param {string=} valign Vertical alignment of the text; either "top", + // "middle" or "bottom". + + Canvas.prototype.addText = function(layer, x, y, text, font, angle, halign, valign) { + + var info = this.getTextInfo(layer, text, font, angle); + + // 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 -= info.width / 2; + } else if (halign == "right") { + x -= info.width; + } + + if (valign == "middle") { + y -= info.height / 2; + } else if (valign == "bottom") { + y -= info.height; + } + + // Move the element to its final position within the container + + info.element.css({ + 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 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(layer, text, font, angle) { + if (text == null) { + var layerCache = this._textCache[layer]; + if (layerCache != null) { + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey] + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + styleCache[key].active = false; + } + } + } + } + } + } else { + this.getTextInfo(layer, text, font, angle).active = false; + } + }; + + /////////////////////////////////////////////////////////////////////////// + // The top-level container for the entire plot. + function Plot(placeholder, data_, options_, plugins) { // data is on the form: // [ series1, series2 ... ] @@ -148,13 +530,12 @@ 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, xaxes = [], yaxes = [], plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, - canvasWidth = 0, canvasHeight = 0, plotWidth = 0, plotHeight = 0, hooks = { processOptions: [], @@ -175,7 +556,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.element; }; plot.getPlotOffset = function() { return plotOffset; }; plot.width = function () { return plotWidth; }; plot.height = function () { return plotHeight; }; @@ -210,9 +591,10 @@ Licensed under the MIT license. }; plot.shutdown = shutdown; plot.resize = function () { - getCanvasDimensions(); - resizeCanvas(canvas); - resizeCanvas(overlay); + var width = placeholder.width(), + height = placeholder.height(); + surface.resize(width, height); + overlay.resize(width, height); }; // public attributes @@ -235,16 +617,22 @@ 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); } } function parseOptions(opts) { - var i; $.extend(true, options, opts); @@ -263,12 +651,44 @@ 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); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + } + } + + 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); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + } + } // backwards compatibility, to be removed in future if (options.xaxis.noTicks && options.xaxis.ticks == null) @@ -730,117 +1150,12 @@ 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, - 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 @@ -850,27 +1165,25 @@ Licensed under the MIT license. if (placeholder.css("position") == 'static') placeholder.css("position", "relative"); // for positioning labels and overlay - getCanvasDimensions(); - - canvas = 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 - canvas = existingCanvas.get(0); - overlay = existingOverlay.get(0); + surface = existingSurface.data("canvas"); + overlay = existingOverlay.data("canvas"); reused = true; } - ctx = canvas.getContext("2d"); - octx = overlay.getContext("2d"); + ctx = surface.context; + octx = overlay.context; // define which element we're listening for events on - eventHolder = $(overlay); + eventHolder = $(overlay.element); if (reused) { // run shutdown in the old plot object @@ -880,11 +1193,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([canvas, overlay]).remove(); + placeholder.children(":not(.flot-base,.flot-overlay,.flot-text)").remove(); } // save in case we get replotted @@ -956,53 +1270,27 @@ 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 + "'"; + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = opts.font || "flot-tick-label tickLabel"; for (var i = 0; i < ticks.length; ++i) { - var t = ticks[i]; - t.lines = []; - t.width = t.height = 0; + var t = ticks[i]; if (!t.label) continue; - // accept various kinds of newlines, including HTML ones - // (you can actually split directly on regexps in Javascript, - // but IE < 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); - } + var info = surface.getTextInfo(layer, t.label, font); if (opts.labelWidth == null) - axisw = Math.max(axisw, t.width); + axisw = Math.max(axisw, info.width); if (opts.labelHeight == null) - axish = Math.max(axish, t.height); + axish = Math.max(axish, info.height); } - ctx.restore(); axis.labelWidth = Math.ceil(axisw); axis.labelHeight = Math.ceil(axish); @@ -1053,7 +1341,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 +1357,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 +1373,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; } } @@ -1161,14 +1449,6 @@ 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)), - variant: placeholder.css("font-variant"), - weight: placeholder.css("font-weight"), - family: placeholder.css("font-family") - }; var allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; }); @@ -1177,9 +1457,7 @@ Licensed under the MIT license. setupTickGeneration(axis); setTicks(axis); snapRangeToTicks(axis, axis.ticks); - // find labelWidth/Height for axis - axis.font = $.extend({}, fontDefaults, axis.options.font); measureTickLabels(axis); }); @@ -1198,14 +1476,18 @@ 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) { setTransformationHelpers(axis); }); + if (showGrid) { + drawAxisLabels(); + } + insertLegend(); } @@ -1258,7 +1540,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; @@ -1313,7 +1595,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; @@ -1429,7 +1711,8 @@ Licensed under the MIT license. } function draw() { - ctx.clearRect(0, 0, canvasWidth, canvasHeight); + + surface.clear(); executeHooks(hooks.drawBackground, [ctx]); @@ -1441,7 +1724,6 @@ Licensed under the MIT license. if (grid.show && !grid.aboveData) { drawGrid(); - drawAxisLabels(); } for (var i = 0; i < series.length; ++i) { @@ -1453,8 +1735,9 @@ Licensed under the MIT license. if (grid.show && grid.aboveData) { drawGrid(); - drawAxisLabels(); } + + surface.render(); } function extractRange(ranges, coord) { @@ -1726,74 +2009,48 @@ 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, + 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); for (var i = 0; i < axis.ticks.length; ++i) { - var tick = axis.ticks[i]; + + tick = axis.ticks[i]; if (!tick.label || tick.v < axis.min || tick.v > axis.max) continue; - var x, y, offset = 0, line; - for (var k = 0; k < tick.lines.length; ++k) { - line = tick.lines[k]; - - if (axis.direction == "x") { - x = plotOffset.left + axis.p2c(tick.v) - line.width/2; - if (axis.position == "bottom") - y = box.top + box.padding; - else - y = box.top + box.height - box.padding - tick.height; + if (axis.direction == "x") { + halign = "center"; + x = plotOffset.left + axis.p2c(tick.v); + if (axis.position == "bottom") { + y = box.top + box.padding; + } else { + y = box.top + box.height - box.padding; + valign = "bottom"; } - else { - y = plotOffset.top + axis.p2c(tick.v) - tick.height/2; - if (axis.position == "left") - x = box.left + box.width - box.padding - line.width; - else - x = box.left + box.padding; + } else { + valign = "middle"; + y = plotOffset.top + axis.p2c(tick.v); + if (axis.position == "left") { + x = box.left + box.width - box.padding; + halign = "right"; + } else { + x = box.left + box.padding; } - - // account for middle aligning and line number - y += line.height/2 + offset; - offset += line.height; - - if (!!(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); - } - ctx.fillText(line.text, x, y); } + + surface.addText(layer, x, y, tick.label, font, null, halign, valign); } }); - - ctx.restore(); } function drawSeries(series) { @@ -2554,7 +2811,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;