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.
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 = $("<div class='" + font + "'>" + text + "</div>")
.appendTo(this.container);
} else {
element = $("<div>" + text + "</div>")
.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(/<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],
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 = $("<div></div>").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(/<br ?\/?>|\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({
......
......@@ -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 = $("<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
// does, detach it so we don't get repaints while adding elements.
this.text.hide();
if (!this.text) {
this.text = $("<div></div>")
.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);
}
});
}
......
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