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