Commit a9a31644 authored by David Schnur's avatar David Schnur

Replace drawText with add and remove methods.

Every cache element now contains the actual text element instead of just
its HTML, plus a flag indicating whether it is visible.  The addText and
removeText methods control the state of this flag, and the render method
uses it to manage elements within the text container.  So where we
previously used drawText to actually render text, now we add each string
once, then let the render method take care of drawing them as necessary.

This dramatically improves performance by eliminating the need to clear
and re-populate HTML text on every drawing cycle.  Since the elements
are now static between add/remove calls, this also allows users to add
interactivity, as they could in 0.7.  Finally, it eliminates the need
for a separate 'hot' cache.

I also removed the unnecessary 'dimensions' object; it's easier and
faster to store the width and height at the top level of the info
object.
parent 5d708696
...@@ -33,233 +33,280 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -33,233 +33,280 @@ browser, but needs to redraw with canvas text when exporting as an image.
canvas: true canvas: true
}; };
// Cache the prototype hasOwnProperty for faster access
var hasOwnProperty = Object.prototype.hasOwnProperty;
function init(plot, classes) { function init(plot, classes) {
var Canvas = classes.Canvas, var Canvas = classes.Canvas,
getTextInfo = Canvas.prototype.getTextInfo, 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. // Finishes rendering the canvas, including overlaid text
//
// When the canvas option is set, this override returns an object
// that looks like this:
//
// {
// lines: {
// height: Height of each line in the text.
// widths: List of widths for each line in the text.
// texts: List of lines in the text.
// },
// font: {
// definition: Canvas font property string.
// color: Color of the text.
// },
// dimensions: {
// width: Width of the text's bounding box.
// height: Height of the text's bounding box.
// }
// }
Canvas.prototype.getTextInfo = function(text, font, angle) { Canvas.prototype.render = function() {
if (plot.getOptions().canvas) {
var textStyle, cacheKey, info; if (!plot.getOptions().canvas) {
return render.call(this);
// Cast the value to a string, in case we were given a number }
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") { for (key in cache) {
textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; if (hasOwnProperty.call(cache, key)) {
} else { cacheHasText = true;
textStyle = font; break;
} }
}
// The text + style + angle uniquely identify the text's if (!cacheHasText) {
// dimensions and content; we'll use them to build this entry's return;
// text cache key. }
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 if (!info.active) {
// classes and examine it to generate a canvas font spec. delete cache[key];
continue;
}
if (typeof font !== "object") { var x = info.x,
y = info.y,
lines = info.lines,
halign = info.halign;
var element; context.fillStyle = info.font.color;
if (typeof font === "string") { context.font = info.font.definition;
element = $("<div class='" + font + "'>" + text + "</div>")
.appendTo(this.container);
} else {
element = $("<div>" + text + "</div>")
.appendTo(this.container);
}
font = { // TODO: Comments in Ole's implementation indicate that
style: element.css("font-style"), // some browsers differ in their interpretation of 'top';
variant: element.css("font-variant"), // so far I don't see this, but it requires more testing.
weight: element.css("font-weight"), // We'll stick with top until this can be verified.
size: parseInt(element.css("font-size")),
family: element.css("font-family"),
color: element.css("color")
};
textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; // 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 for (var i = 0; i < lines.length; ++i) {
// zero so we can count them up line-by-line.
info = {
lines: [],
font: {
definition: textStyle,
color: font.color
},
dimensions: {
width: 0,
height: 0
}
};
context.save();
context.font = textStyle;
// Canvas can't handle multi-line strings; break on various var line = lines[i],
// newlines, including HTML brs, to build a list of lines. linex = x;
// 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(/<br ?\/?>|\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], // FIXME: LEGACY BROWSER FIX
measured = context.measureText(lineText), // AFFECTS: Opera < 12.00
lineWidth, lineHeight;
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 context.restore();
// pixel perfect and cut off letters look bad. This };
// also doubles as spacing between lines.
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); Canvas.prototype.getTextInfo = function(text, font, angle) {
info.dimensions.height += lineHeight;
info.lines.push({ if (!plot.getOptions().canvas) {
text: lineText, return getTextInfo.call(this, text, font, angle);
width: lineWidth, }
height: lineHeight
});
}
context.restore; var textStyle, cacheKey, info;
}
// Save the entry to the 'hot' text cache, marking it as active // Cast the value to a string, in case we were given a number
// and preserving it for the next render pass.
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 { } else {
return getTextInfo.call(this, text, font, angle); textStyle = font;
} }
}
// Draws a text string onto the canvas. // The text + style + angle uniquely identify the text's dimensions
// // and content; we'll use them to build the entry's text cache key.
// When the canvas option is set, this override draws directly to the
// canvas using fillText.
Canvas.prototype.drawText = function(x, y, text, font, angle, halign, valign) { cacheKey = text + "-" + textStyle + "-" + angle;
if (plot.getOptions().canvas) {
var info = this.getTextInfo(text, font, angle), info = this._textCache[cacheKey];
dimensions = info.dimensions,
context = this.context,
lines = info.lines;
// Apply alignment to the vertical position of the entire text if (info == null) {
if (valign == "middle") { var context = this.context;
y -= dimensions.height / 2;
} else if (valign == "bottom") {
y -= dimensions.height;
}
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; var element = $("<div></div>").html(text)
context.font = info.font.definition; .addClass(typeof font === "string" ? font : null)
.appendTo(this.container);
// TODO: Comments in Ole's implementation indicate that some font = {
// browsers differ in their interpretation of 'top'; so far I style: element.css("font-style"),
// don't see this, but it requires more testing. We'll stick variant: element.css("font-variant"),
// with top until this can be verified. Original comment was: 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 // Canvas can't handle multi-line strings; break on various
// a pixel or two in where they consider the top to be, so // newlines, including HTML brs, to build a list of lines.
// instead we middle align to minimize variation between // Note that we could split directly on regexps, but IE < 9 is
// browsers and compensate when calculating the coordinates. // broken; revisit when we drop IE 7/8 support.
context.textBaseline = "top"; var lines = (text + "").replace(/<br ?\/?>|\r\n|\r/g, "\n").split("\n");
for (var i = 0; i < lines.length; ++i) { for (var i = 0; i < lines.length; ++i) {
var line = lines[i], var lineText = lines[i],
linex = x; measured = context.measureText(lineText),
lineWidth, lineHeight;
// Apply alignment to the horizontal position per-line lineWidth = measured.width;
if (halign == "center") { // Height might not be defined; not in the standard yet
linex -= line.width / 2;
} else if (halign == "right") {
linex -= line.width;
}
// FIXME: LEGACY BROWSER FIX lineHeight = measured.height || font.size;
// AFFECTS: Opera < 12.00
// Round the coordinates, since Opera otherwise // Add a bit of margin since font rendering is not pixel
// switches to more ugly rendering (probably // perfect and cut off letters look bad. This also doubles
// non-hinted) and offset the y coordinates since // as spacing between lines.
// it seems to be off pretty consistently compared
// to the other browsers
if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { lineHeight += Math.round(font.size * 0.15);
linex = Math.floor(linex);
y = Math.ceil(y - 2); info.width = Math.max(lineWidth, info.width);
} info.height += lineHeight;
context.fillText(line.text, linex, y); info.lines.push({
y += line.height; text: lineText,
width: lineWidth,
height: lineHeight
});
} }
this._textCache[cacheKey] = info;
context.restore(); context.restore();
}
} else { return info;
drawText.call(this, x, y, text, font, angle, halign, valign); };
// 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({ $.plot.plugins.push({
......
...@@ -109,18 +109,6 @@ Licensed under the MIT license. ...@@ -109,18 +109,6 @@ Licensed under the MIT license.
// re-calculating them when the plot is re-rendered in a loop. // re-calculating them when the plot is re-rendered in a loop.
this._textCache = {}; 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. // Resizes the canvas to the given dimensions.
...@@ -171,70 +159,73 @@ Licensed under the MIT license. ...@@ -171,70 +159,73 @@ Licensed under the MIT license.
context.scale(pixelRatio, pixelRatio); 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() { Canvas.prototype.clear = function() {
this.context.clearRect(0, 0, this.width, this.height); 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() { Canvas.prototype.render = function() {
var cache = this._activeTextCache; var cache = this._textCache,
cacheHasText = false,
// Swap out the text cache for the 'hot cache' that we've been filling info, key;
// out since the last call to render.
this._activeTextCache = {};
this._textCache = cache;
// Check whether the cache actually has any entries. // Check whether the cache actually has any entries.
var cacheHasText = false; for (key in cache) {
for (var key in cache) {
if (hasOwnProperty.call(cache, key)) { if (hasOwnProperty.call(cache, key)) {
cacheHasText = true; cacheHasText = true;
break; 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 = $("<div></div>")
.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 this.text.hide();
// does, detach it so we don't get repaints while adding elements.
if (!this.text) { for (key in cache) {
this.text = $("<div></div>") if (hasOwnProperty.call(cache, key)) {
.addClass("flot-text")
.css({
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0
});
} else {
this.text.detach();
}
// Add all the elements to the text layer, then add it to the DOM info = cache[key];
// at the end, so we only trigger a single redraw.
for (var key in cache) { if (info.active) {
if (hasOwnProperty.call(cache, key)) { if (!info.rendered) {
this.text.append(cache[key].element); 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. // Creates (if necessary) and returns a text info object.
...@@ -242,11 +233,11 @@ Licensed under the MIT license. ...@@ -242,11 +233,11 @@ Licensed under the MIT license.
// The object looks like this: // 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. // 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 // Canvas maintains a cache of recently-used text info objects; getTextInfo
...@@ -280,7 +271,7 @@ Licensed under the MIT license. ...@@ -280,7 +271,7 @@ Licensed under the MIT license.
cacheKey = text + "-" + textStyle + "-" + angle; 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 // If we can't find a matching element in our cache, create a new one
...@@ -294,7 +285,7 @@ Licensed under the MIT license. ...@@ -294,7 +285,7 @@ Licensed under the MIT license.
if (typeof font === "object") { if (typeof font === "object") {
element.css({ element.css({
font: textStyle, font: textStyle,
color: font.color, color: font.color
}); });
} else if (typeof font === "string") { } else if (typeof font === "string") {
element.addClass(font); element.addClass(font);
...@@ -303,29 +294,25 @@ Licensed under the MIT license. ...@@ -303,29 +294,25 @@ Licensed under the MIT license.
element.appendTo(this.container); element.appendTo(this.container);
info = { info = {
active: false,
rendered: false,
element: element, element: element,
dimensions: { width: element.outerWidth(true),
width: element.outerWidth(true), height: element.outerHeight(true)
height: element.outerHeight(true)
}
}; };
element.detach(); element.detach();
}
// Save the entry to the 'hot' text cache, marking it as active and this._textCache[cacheKey] = info;
// preserving it for the next render pass. }
this._activeTextCache[cacheKey] = info;
return 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 // The text isn't drawn immediately; it is marked as rendering, which will
// buffer it to improve performance. Text is only guaranteed to be drawn // result in its addition to the canvas on the next render pass.
// after the Canvas render method has been called.
// //
// @param {number} x X coordinate at which to draw the text. // @param {number} x X coordinate at which to draw the text.
// @param {number} y Y 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. ...@@ -339,33 +326,64 @@ Licensed under the MIT license.
// @param {string=} valign Vertical alignment of the text; either "top", // @param {string=} valign Vertical alignment of the text; either "top",
// "middle" or "bottom". // "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), // Mark the div for inclusion in the next render pass
dimensions = info.dimensions;
info.active = true;
// Tweak the div's position to match the text's alignment // Tweak the div's position to match the text's alignment
if (halign == "center") { if (halign == "center") {
x -= dimensions.width / 2; x -= info.width / 2;
} else if (halign == "right") { } else if (halign == "right") {
x -= dimensions.width; x -= info.width;
} }
if (valign == "middle") { if (valign == "middle") {
y -= dimensions.height / 2; y -= info.height / 2;
} else if (valign == "bottom") { } else if (valign == "bottom") {
y -= dimensions.height; y -= info.height;
} }
// Move the element to its final position within the container // Move the element to its final position within the container
info.element.css({ info.element.css({
top: parseInt(y), top: parseInt(y, 10),
left: parseInt(x) 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. // The top-level container for the entire plot.
...@@ -1225,18 +1243,17 @@ Licensed under the MIT license. ...@@ -1225,18 +1243,17 @@ Licensed under the MIT license.
for (var i = 0; i < ticks.length; ++i) { for (var i = 0; i < ticks.length; ++i) {
var t = ticks[i], var t = ticks[i];
dimensions;
if (!t.label) if (!t.label)
continue; continue;
dimensions = surface.getTextInfo(t.label, font).dimensions; var info = surface.getTextInfo(t.label, font);
if (opts.labelWidth == null) if (opts.labelWidth == null)
axisw = Math.max(axisw, dimensions.width); axisw = Math.max(axisw, info.width);
if (opts.labelHeight == null) if (opts.labelHeight == null)
axish = Math.max(axish, dimensions.height); axish = Math.max(axish, info.height);
} }
axis.labelWidth = Math.ceil(axisw); axis.labelWidth = Math.ceil(axisw);
...@@ -1431,6 +1448,10 @@ Licensed under the MIT license. ...@@ -1431,6 +1448,10 @@ Licensed under the MIT license.
setTransformationHelpers(axis); setTransformationHelpers(axis);
}); });
if (showGrid) {
drawAxisLabels();
}
insertLegend(); insertLegend();
} }
...@@ -1667,7 +1688,6 @@ Licensed under the MIT license. ...@@ -1667,7 +1688,6 @@ Licensed under the MIT license.
if (grid.show && !grid.aboveData) { if (grid.show && !grid.aboveData) {
drawGrid(); drawGrid();
drawAxisLabels();
} }
for (var i = 0; i < series.length; ++i) { for (var i = 0; i < series.length; ++i) {
...@@ -1679,7 +1699,6 @@ Licensed under the MIT license. ...@@ -1679,7 +1699,6 @@ Licensed under the MIT license.
if (grid.show && grid.aboveData) { if (grid.show && grid.aboveData) {
drawGrid(); drawGrid();
drawAxisLabels();
} }
surface.render(); surface.render();
...@@ -1955,6 +1974,8 @@ Licensed under the MIT license. ...@@ -1955,6 +1974,8 @@ Licensed under the MIT license.
function drawAxisLabels() { function drawAxisLabels() {
surface.removeText();
$.each(allAxes(), function (_, axis) { $.each(allAxes(), function (_, axis) {
if (!axis.show || axis.ticks.length == 0) if (!axis.show || axis.ticks.length == 0)
return; return;
...@@ -1989,7 +2010,7 @@ Licensed under the MIT license. ...@@ -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);
} }
}); });
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment