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