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