Commit 77e50b17 authored by David Schnur's avatar David Schnur

Allow text to be divided between multiple layers.

This lets users 'namespace' text more naturally, i.e. placing x-axis
labels in a different container from y-axis labels, providing more
flexibility when it comes to styling and interactivity.

Internally the text cache now has a second tier: layers > text > info.
parent 4203a66e
...@@ -53,87 +53,79 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -53,87 +53,79 @@ browser, but needs to redraw with canvas text when exporting as an image.
} }
var context = this.context, var context = this.context,
cache = this._textCache, cache = this._textCache;
cacheHasText = false,
key;
// Check whether the cache actually has any entries. // For each text layer, render elements marked as active
for (key in cache) { context.save();
if (hasOwnProperty.call(cache, key)) {
cacheHasText = true;
break;
}
}
if (!cacheHasText) { for (var layerKey in cache) {
return; if (hasOwnProperty.call(cache, layerKey)) {
}
// Render the contents of the cache var layerCache = cache[layerKey];
context.save(); for (var key in layerCache) {
if (hasOwnProperty.call(layerCache, key)) {
for (key in cache) { var info = layerCache[key];
if (hasOwnProperty.call(cache, key)) {
var info = cache[key]; if (!info.active) {
delete cache[key];
continue;
}
if (!info.active) { var x = info.x,
delete cache[key]; y = info.y,
continue; lines = info.lines,
} halign = info.halign;
var x = info.x, context.fillStyle = info.font.color;
y = info.y, context.font = info.font.definition;
lines = info.lines,
halign = info.halign;
context.fillStyle = info.font.color; // TODO: Comments in Ole's implementation indicate that
context.font = info.font.definition; // 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.
// TODO: Comments in Ole's implementation indicate that // Original comment was:
// some browsers differ in their interpretation of 'top'; // Top alignment would be more natural, but browsers can
// so far I don't see this, but it requires more testing. // differ a pixel or two in where they consider the top to
// We'll stick with top until this can be verified. // be, so instead we middle align to minimize variation
// between browsers and compensate when calculating the
// coordinates.
// Original comment was: context.textBaseline = "top";
// 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) {
for (var i = 0; i < lines.length; ++i) { var line = lines[i],
linex = x;
var line = lines[i], // Apply horizontal alignment per-line
linex = x;
// Apply horizontal alignment per-line if (halign == "center") {
linex -= line.width / 2;
} else if (halign == "right") {
linex -= line.width;
}
if (halign == "center") { // FIXME: LEGACY BROWSER FIX
linex -= line.width / 2; // AFFECTS: Opera < 12.00
} else if (halign == "right") {
linex -= line.width;
}
// FIXME: LEGACY BROWSER FIX // Round the coordinates, since Opera otherwise
// AFFECTS: Opera < 12.00 // switches to uglier (probably non-hinted) rendering.
// Also offset the y coordinate, since Opera is off
// pretty consistently compared to the other browsers.
// Round the coordinates, since Opera otherwise if (!!(window.opera && window.opera.version().split(".")[0] < 12)) {
// switches to uglier (probably non-hinted) rendering. linex = Math.floor(linex);
// Also offset the y coordinate, since Opera is off y = Math.ceil(y - 2);
// pretty consistently compared to the other browsers. }
if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { context.fillText(line.text, linex, y);
linex = Math.floor(linex); y += line.height;
y = Math.ceil(y - 2); }
} }
context.fillText(line.text, linex, y);
y += line.height;
} }
} }
} }
...@@ -162,13 +154,13 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -162,13 +154,13 @@ browser, but needs to redraw with canvas text when exporting as an image.
// }, // },
// } // }
Canvas.prototype.getTextInfo = function(text, font, angle) { Canvas.prototype.getTextInfo = function(layer, text, font, angle) {
if (!plot.getOptions().canvas) { if (!plot.getOptions().canvas) {
return getTextInfo.call(this, text, font, angle); return getTextInfo.call(this, layer, text, font, angle);
} }
var textStyle, cacheKey, info; var textStyle, cache, cacheKey, info;
// Cast the value to a string, in case we were given a number // Cast the value to a string, in case we were given a number
...@@ -182,13 +174,21 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -182,13 +174,21 @@ browser, but needs to redraw with canvas text when exporting as an image.
textStyle = font; textStyle = font;
} }
// Retrieve (or create) the cache for the text's layer
cache = this._textCache[layer];
if (cache == null) {
cache = this._textCache[layer] = {};
}
// The text + style + angle uniquely identify the text's dimensions // The text + style + angle uniquely identify the text's dimensions
// and content; we'll use them to build the entry's text cache key. // and content; we'll use them to build the entry's text cache key.
// NOTE: We don't support rotated text yet, so the angle is unused. // NOTE: We don't support rotated text yet, so the angle is unused.
cacheKey = textStyle + "|" + text; cacheKey = textStyle + "|" + text;
info = this._textCache[cacheKey]; info = cache[cacheKey];
if (info == null) { if (info == null) {
...@@ -205,7 +205,7 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -205,7 +205,7 @@ browser, but needs to redraw with canvas text when exporting as an image.
position: "absolute", position: "absolute",
top: -9999 top: -9999
}) })
.appendTo(this.getTextLayer()); .appendTo(this.getTextLayer(layer));
font = { font = {
style: element.css("font-style"), style: element.css("font-style"),
...@@ -224,7 +224,7 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -224,7 +224,7 @@ browser, but needs to redraw with canvas text when exporting as an image.
// 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 = cache[cacheKey] = {
x: null, x: null,
y: null, y: null,
width: 0, width: 0,
...@@ -275,8 +275,6 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -275,8 +275,6 @@ browser, but needs to redraw with canvas text when exporting as an image.
}); });
} }
this._textCache[cacheKey] = info;
context.restore(); context.restore();
} }
...@@ -285,13 +283,13 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -285,13 +283,13 @@ browser, but needs to redraw with canvas text when exporting as an image.
// Adds a text string to the canvas text overlay. // Adds a text string to the canvas text overlay.
Canvas.prototype.addText = function(x, y, text, font, angle, halign, valign) { Canvas.prototype.addText = function(layer, x, y, text, font, angle, halign, valign) {
if (!plot.getOptions().canvas) { if (!plot.getOptions().canvas) {
return addText.call(this, x, y, text, font, angle, halign, valign); return addText.call(this, layer, x, y, text, font, angle, halign, valign);
} }
var info = this.getTextInfo(text, font, angle); var info = this.getTextInfo(layer, text, font, angle);
info.x = x; info.x = x;
info.y = y; info.y = y;
......
...@@ -99,9 +99,9 @@ Licensed under the MIT license. ...@@ -99,9 +99,9 @@ Licensed under the MIT license.
this.resize(container.width(), container.height()); this.resize(container.width(), container.height());
// Container for HTML text overlaid onto the canvas; created on demand // Collection of HTML div layers for text overlaid onto the canvas
this.text = null; this.text = {};
// Cache of text fragments and metrics, so we can avoid expensively // Cache of text fragments and metrics, so we can avoid expensively
// re-calculating them when the plot is re-rendered in a loop. // re-calculating them when the plot is re-rendered in a loop.
...@@ -167,66 +167,58 @@ Licensed under the MIT license. ...@@ -167,66 +167,58 @@ Licensed under the MIT license.
Canvas.prototype.render = function() { Canvas.prototype.render = function() {
var cache = this._textCache, var cache = this._textCache;
cacheHasText = false,
key;
// Check whether the cache actually has any entries. // For each text layer, add elements marked as active that haven't
// already been rendered, and remove those that are no longer active.
for (key in cache) { for (var layerKey in cache) {
if (hasOwnProperty.call(cache, key)) { if (hasOwnProperty.call(cache, layerKey)) {
cacheHasText = true;
break;
}
}
if (!cacheHasText) {
return;
}
// Create the HTML text layer, if it doesn't already exist.
var layer = this.getTextLayer(), var layer = this.getTextLayer(layerKey),
info; layerCache = cache[layerKey];
// Add all the elements to the text layer, then add it to the DOM at layer.hide();
// the end, so we only trigger a single redraw.
layer.hide(); for (var key in layerCache) {
if (hasOwnProperty.call(layerCache, key)) {
for (key in cache) { var info = layerCache[key];
if (hasOwnProperty.call(cache, key)) {
info = cache[key]; if (info.active) {
if (!info.rendered) {
if (info.active) { layer.append(info.element);
if (!info.rendered) { info.rendered = true;
layer.append(info.element); }
info.rendered = true; } else {
} delete layerCache[key];
} else { if (info.rendered) {
delete cache[key]; info.element.detach();
if (info.rendered) { }
info.element.detach(); }
} }
} }
layer.show();
} }
} }
layer.show();
}; };
// Creates (if necessary) and returns the text overlay container. // Creates (if necessary) and returns the text overlay container.
// //
// @param {string} classes String of space-separated CSS classes used to
// uniquely identify the text layer.
// @return {object} The jQuery-wrapped text-layer div. // @return {object} The jQuery-wrapped text-layer div.
Canvas.prototype.getTextLayer = function() { Canvas.prototype.getTextLayer = function(classes) {
var layer = this.text[classes];
// Create the text layer if it doesn't exist // Create the text layer if it doesn't exist
if (!this.text) { if (layer == null) {
this.text = $("<div></div>") layer = this.text[classes] = $("<div></div>")
.addClass("flot-text") .addClass("flot-text " + classes)
.css({ .css({
position: "absolute", position: "absolute",
top: 0, top: 0,
...@@ -237,7 +229,7 @@ Licensed under the MIT license. ...@@ -237,7 +229,7 @@ Licensed under the MIT license.
.insertAfter(this.element); .insertAfter(this.element);
} }
return this.text; return layer;
}; };
// Creates (if necessary) and returns a text info object. // Creates (if necessary) and returns a text info object.
...@@ -255,6 +247,8 @@ Licensed under the MIT license. ...@@ -255,6 +247,8 @@ Licensed under the MIT license.
// Canvas maintains a cache of recently-used text info objects; getTextInfo // Canvas maintains a cache of recently-used text info objects; getTextInfo
// either returns the cached element or creates a new entry. // either returns the cached element or creates a new entry.
// //
// @param {string} layer A string of space-separated CSS classes uniquely
// identifying the layer containing this text.
// @param {string} text Text string to retrieve info for. // @param {string} text Text string to retrieve info for.
// @param {(string|object)=} font Either a string of space-separated CSS // @param {(string|object)=} font Either a string of space-separated CSS
// classes or a font-spec object, defining the text's font and style. // classes or a font-spec object, defining the text's font and style.
...@@ -262,9 +256,9 @@ Licensed under the MIT license. ...@@ -262,9 +256,9 @@ Licensed under the MIT license.
// Angle is currently unused, it will be implemented in the future. // Angle is currently unused, it will be implemented in the future.
// @return {object} a text info object. // @return {object} a text info object.
Canvas.prototype.getTextInfo = function(text, font, angle) { Canvas.prototype.getTextInfo = function(layer, text, font, angle) {
var textStyle, cacheKey, info; var textStyle, cache, cacheKey, info;
// Cast the value to a string, in case we were given a number or such // Cast the value to a string, in case we were given a number or such
...@@ -278,13 +272,21 @@ Licensed under the MIT license. ...@@ -278,13 +272,21 @@ Licensed under the MIT license.
textStyle = font; textStyle = font;
} }
// Retrieve (or create) the cache for the text's layer
cache = this._textCache[layer];
if (cache == null) {
cache = this._textCache[layer] = {};
}
// The text + style + angle uniquely identify the text's dimensions and // The text + style + angle uniquely identify the text's dimensions and
// content; we'll use them to build this entry's text cache key. // content; we'll use them to build this entry's text cache key.
// NOTE: We don't support rotated text yet, so the angle is unused. // NOTE: We don't support rotated text yet, so the angle is unused.
cacheKey = textStyle + "|" + text; cacheKey = textStyle + "|" + text;
info = this._textCache[cacheKey]; info = cache[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
...@@ -295,7 +297,7 @@ Licensed under the MIT license. ...@@ -295,7 +297,7 @@ Licensed under the MIT license.
position: "absolute", position: "absolute",
top: -9999 top: -9999
}) })
.appendTo(this.getTextLayer()); .appendTo(this.getTextLayer(layer));
if (typeof font === "object") { if (typeof font === "object") {
element.css({ element.css({
...@@ -306,7 +308,7 @@ Licensed under the MIT license. ...@@ -306,7 +308,7 @@ Licensed under the MIT license.
element.addClass(font); element.addClass(font);
} }
info = { info = cache[cacheKey] = {
active: false, active: false,
rendered: false, rendered: false,
element: element, element: element,
...@@ -315,8 +317,6 @@ Licensed under the MIT license. ...@@ -315,8 +317,6 @@ Licensed under the MIT license.
}; };
element.detach(); element.detach();
this._textCache[cacheKey] = info;
} }
return info; return info;
...@@ -327,6 +327,8 @@ Licensed under the MIT license. ...@@ -327,6 +327,8 @@ Licensed under the MIT license.
// The text isn't drawn immediately; it is marked as rendering, which will // 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. // result in its addition to the canvas on the next render pass.
// //
// @param {string} layer A string of space-separated CSS classes uniquely
// identifying the layer containing this text.
// @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.
// @param {string} text Text string to draw. // @param {string} text Text string to draw.
...@@ -339,9 +341,9 @@ Licensed under the MIT license. ...@@ -339,9 +341,9 @@ 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.addText = function(x, y, text, font, angle, halign, valign) { Canvas.prototype.addText = function(layer, x, y, text, font, angle, halign, valign) {
var info = this.getTextInfo(text, font, angle); var info = this.getTextInfo(layer, text, font, angle);
// Mark the div for inclusion in the next render pass // Mark the div for inclusion in the next render pass
...@@ -371,29 +373,30 @@ Licensed under the MIT license. ...@@ -371,29 +373,30 @@ Licensed under the MIT license.
// Removes one or more text strings from the canvas text overlay. // Removes one or more text strings from the canvas text overlay.
// //
// If no parameters are given, all text within the container is removed. // If no parameters are given, all text within the layer is removed.
// The text is not actually removed; it is simply marked as inactive, which // The text is not actually removed; it is simply marked as inactive, which
// will result in its removal on the next render pass. // will result in its removal on the next render pass.
// //
// @param {string} layer A string of space-separated CSS classes uniquely
// identifying the layer containing this text.
// @param {string} text Text string to remove. // @param {string} text Text string to remove.
// @param {(string|object)=} font Either a string of space-separated CSS // @param {(string|object)=} font Either a string of space-separated CSS
// classes or a font-spec object, defining the text's font and style. // 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. // @param {number=} angle Angle at which the text is rotated, in degrees.
// Angle is currently unused, it will be implemented in the future. // Angle is currently unused, it will be implemented in the future.
Canvas.prototype.removeText = function(text, font, angle) { Canvas.prototype.removeText = function(layer, text, font, angle) {
if (text == null) { if (text == null) {
var cache = this._textCache; var cache = this._textCache[layer];
for (var key in cache) { if (cache != null) {
if (hasOwnProperty.call(cache, key)) { for (var key in cache) {
cache[key].active = false; if (hasOwnProperty.call(cache, key)) {
cache[key].active = false;
}
} }
} }
} else { } else {
var info = this.getTextInfo(text, font, angle); this.getTextInfo(layer, text, font, angle).active = false;
if (info != null) {
info.active = false;
}
} }
}; };
...@@ -1252,7 +1255,8 @@ Licensed under the MIT license. ...@@ -1252,7 +1255,8 @@ Licensed under the MIT license.
var opts = axis.options, ticks = axis.ticks || [], var opts = axis.options, ticks = axis.ticks || [],
axisw = opts.labelWidth || 0, axish = opts.labelHeight || 0, axisw = opts.labelWidth || 0, axish = opts.labelHeight || 0,
font = opts.font || "flot-tick-label flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis"; layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis",
font = opts.font || "flot-tick-label";
for (var i = 0; i < ticks.length; ++i) { for (var i = 0; i < ticks.length; ++i) {
...@@ -1261,7 +1265,7 @@ Licensed under the MIT license. ...@@ -1261,7 +1265,7 @@ Licensed under the MIT license.
if (!t.label) if (!t.label)
continue; continue;
var info = surface.getTextInfo(t.label, font); var info = surface.getTextInfo(layer, t.label, font);
if (opts.labelWidth == null) if (opts.labelWidth == null)
axisw = Math.max(axisw, info.width); axisw = Math.max(axisw, info.width);
...@@ -1987,16 +1991,17 @@ Licensed under the MIT license. ...@@ -1987,16 +1991,17 @@ 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;
var box = axis.box, var box = axis.box,
font = axis.options.font || "flot-tick-label flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis", layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis",
font = axis.options.font || "flot-tick-label",
tick, x, y, halign, valign; tick, x, y, halign, valign;
surface.removeText(layer);
for (var i = 0; i < axis.ticks.length; ++i) { for (var i = 0; i < axis.ticks.length; ++i) {
tick = axis.ticks[i]; tick = axis.ticks[i];
...@@ -2023,7 +2028,7 @@ Licensed under the MIT license. ...@@ -2023,7 +2028,7 @@ Licensed under the MIT license.
} }
} }
surface.addText(x, y, tick.label, font, null, halign, valign); surface.addText(layer, 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