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,35 +33,140 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -33,35 +33,140 @@ 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;
// 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,
cacheHasText = false,
key;
// Check whether the cache actually has any entries.
for (key in cache) {
if (hasOwnProperty.call(cache, key)) {
cacheHasText = true;
break;
}
}
if (!cacheHasText) {
return;
}
// Render the contents of the cache
context.save();
for (key in cache) {
if (hasOwnProperty.call(cache, key)) {
var info = cache[key];
if (!info.active) {
delete cache[key];
continue;
}
var x = info.x,
y = info.y,
lines = info.lines,
halign = info.halign;
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.
// 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. // Creates (if necessary) and returns a text info object.
// //
// When the canvas option is set, this override returns an object // When the canvas option is set, the object looks like this:
// that looks like this:
// //
// { // {
// lines: { // x: X coordinate at which the text is located.
// height: Height of each line in the text. // x: Y coordinate at which the text is located.
// widths: List of widths for each line in the text. // width: Width of the text's bounding box.
// texts: List of lines in the text. // 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: { // font: {
// definition: Canvas font property string. // definition: Canvas font property string.
// color: Color of the text. // 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.getTextInfo = function(text, font, angle) {
if (plot.getOptions().canvas) {
if (!plot.getOptions().canvas) {
return getTextInfo.call(this, text, font, angle);
}
var textStyle, cacheKey, info; var textStyle, cacheKey, info;
...@@ -77,13 +182,12 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -77,13 +182,12 @@ browser, but needs to redraw with canvas text when exporting as an image.
textStyle = font; textStyle = font;
} }
// The text + style + angle uniquely identify the text's // The text + style + angle uniquely identify the text's dimensions
// dimensions and content; we'll use them to build this entry's // and content; we'll use them to build the entry's text cache key.
// text cache key.
cacheKey = text + "-" + textStyle + "-" + angle; cacheKey = text + "-" + textStyle + "-" + angle;
info = this._textCache[cacheKey] || this._activeTextCache[cacheKey]; info = this._textCache[cacheKey];
if (info == null) { if (info == null) {
...@@ -94,41 +198,37 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -94,41 +198,37 @@ browser, but needs to redraw with canvas text when exporting as an image.
if (typeof font !== "object") { if (typeof font !== "object") {
var element; var element = $("<div></div>").html(text)
if (typeof font === "string") { .addClass(typeof font === "string" ? font : null)
element = $("<div class='" + font + "'>" + text + "</div>")
.appendTo(this.container); .appendTo(this.container);
} else {
element = $("<div>" + text + "</div>")
.appendTo(this.container);
}
font = { font = {
style: element.css("font-style"), style: element.css("font-style"),
variant: element.css("font-variant"), variant: element.css("font-variant"),
weight: element.css("font-weight"), weight: element.css("font-weight"),
size: parseInt(element.css("font-size")), size: parseInt(element.css("font-size"), 10),
family: element.css("font-family"), family: element.css("font-family"),
color: element.css("color") color: element.css("color")
}; };
textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family;
element.remove(); element.remove();
} }
textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family;
// Create a new info object, initializing the dimensions to // Create a new info object, initializing the dimensions to
// zero so we can count them up line-by-line. // zero so we can count them up line-by-line.
info = { info = {
x: null,
y: null,
width: 0,
height: 0,
active: false,
lines: [], lines: [],
font: { font: {
definition: textStyle, definition: textStyle,
color: font.color color: font.color
},
dimensions: {
width: 0,
height: 0
} }
}; };
...@@ -137,8 +237,8 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -137,8 +237,8 @@ browser, but needs to redraw with canvas text when exporting as an image.
// Canvas can't handle multi-line strings; break on various // Canvas can't handle multi-line strings; break on various
// newlines, including HTML brs, to build a list of lines. // newlines, including HTML brs, to build a list of lines.
// Note that we could split directly on regexps, but IE < 9 // Note that we could split directly on regexps, but IE < 9 is
// is broken; revisit when we drop IE 7/8 support. // broken; revisit when we drop IE 7/8 support.
var lines = (text + "").replace(/<br ?\/?>|\r\n|\r/g, "\n").split("\n"); var lines = (text + "").replace(/<br ?\/?>|\r\n|\r/g, "\n").split("\n");
...@@ -154,14 +254,14 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -154,14 +254,14 @@ browser, but needs to redraw with canvas text when exporting as an image.
lineHeight = measured.height || font.size; lineHeight = measured.height || font.size;
// Add a bit of margin since font rendering is not // Add a bit of margin since font rendering is not pixel
// pixel perfect and cut off letters look bad. This // perfect and cut off letters look bad. This also doubles
// also doubles as spacing between lines. // as spacing between lines.
lineHeight += Math.round(font.size * 0.15); lineHeight += Math.round(font.size * 0.15);
info.dimensions.width = Math.max(lineWidth, info.dimensions.width); info.width = Math.max(lineWidth, info.width);
info.dimensions.height += lineHeight; info.height += lineHeight;
info.lines.push({ info.lines.push({
text: lineText, text: lineText,
...@@ -170,96 +270,43 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -170,96 +270,43 @@ browser, but needs to redraw with canvas text when exporting as an image.
}); });
} }
context.restore; this._textCache[cacheKey] = info;
}
// Save the entry to the 'hot' text cache, marking it as active
// and preserving it for the next render pass.
this._activeTextCache[cacheKey] = info;
return info; context.restore();
} else {
return getTextInfo.call(this, text, font, angle);
}
} }
// Draws a text string onto the canvas. return info;
// };
// 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) {
if (plot.getOptions().canvas) {
var info = this.getTextInfo(text, font, angle), // Adds a text string to the canvas text overlay.
dimensions = info.dimensions,
context = this.context,
lines = info.lines;
// Apply alignment to the vertical position of the entire text Canvas.prototype.addText = function(x, y, text, font, angle, halign, valign) {
if (valign == "middle") { if (!plot.getOptions().canvas) {
y -= dimensions.height / 2; return addText.call(this, x, y, text, font, angle, halign, valign);
} else if (valign == "bottom") {
y -= dimensions.height;
} }
context.save(); var info = this.getTextInfo(text, font, angle);
context.fillStyle = info.font.color; info.x = x;
context.font = info.font.definition; info.y = y;
// TODO: Comments in Ole's implementation indicate that some // Mark the text for inclusion in the next render pass
// 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 info.active = true;
// 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"; // Save horizontal alignment for later; we'll apply it per-line
for (var i = 0; i < lines.length; ++i) { info.halign = halign;
var line = lines[i],
linex = x;
// Apply alignment to the horizontal position 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 // Tweak the initial y-position to match vertical alignment
// 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
if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { if (valign == "middle") {
linex = Math.floor(linex); info.y = y - info.height / 2;
y = Math.ceil(y - 2); } else if (valign == "bottom") {
} info.y = y - info.height;
context.fillText(line.text, linex, y);
y += line.height;
}
context.restore();
} else {
drawText.call(this, x, y, text, font, angle, halign, valign);
}
} }
};
} }
$.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,44 +159,34 @@ Licensed under the MIT license. ...@@ -171,44 +159,34 @@ 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;
if (cacheHasText) { }
// Create the HTML text layer, if it doesn't already exist; if it // Create the HTML text layer, if it doesn't already exist.
// does, detach it so we don't get repaints while adding elements.
if (!this.text) { if (!this.text) {
this.text = $("<div></div>") this.text = $("<div></div>")
...@@ -219,22 +197,35 @@ Licensed under the MIT license. ...@@ -219,22 +197,35 @@ Licensed under the MIT license.
left: 0, left: 0,
bottom: 0, bottom: 0,
right: 0 right: 0
}); })
} else { .insertAfter(this.element);
this.text.detach();
} }
// Add all the elements to the text layer, then add it to the DOM // Add all the elements to the text layer, then add it to the DOM at
// at the end, so we only trigger a single redraw. // the end, so we only trigger a single redraw.
for (var key in cache) { this.text.hide();
for (key in cache) {
if (hasOwnProperty.call(cache, key)) { if (hasOwnProperty.call(cache, key)) {
this.text.append(cache[key].element);
info = cache[key];
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. // 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:
// //
// { // {
// element: The jQuery-wrapped HTML div containing the text.
// dimensions: {
// width: Width of the text's wrapper div. // width: Width of the text's wrapper div.
// height: Height 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 // 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);
// Mark the div for inclusion in the next render pass
var info = this.getTextInfo(text, font, angle), info.active = true;
dimensions = info.dimensions;
// 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