Commit a0529ee8 authored by David Schnur's avatar David Schnur

Moved canvas tick rendering into a plugin.

The base implementation uses the new drawText and getTextInfo methods to
draw text in HTML.  Canvas rendering has been moved to overrides of
these methods within the canvas-render plugin.
parent 3b2d43bf
...@@ -17,45 +17,249 @@ every element of the plot to be rendered directly to canvas. ...@@ -17,45 +17,249 @@ every element of the plot to be rendered directly to canvas.
The plugin supports these options: The plugin supports these options:
canvas: boolean, {
xaxis, yaxis: { canvas: boolean
font: null or font spec object }
The "canvas" option controls whether full canvas drawing is enabled, making it
possible to toggle on and off. This is useful when a plot uses HTML text in the
browser, but needs to redraw with canvas text when exporting as an image.
*/
(function($) {
var options = {
canvas: true
};
function init(plot, classes) {
var Canvas = classes.Canvas,
getTextInfo = Canvas.prototype.getTextInfo,
drawText = Canvas.prototype.drawText;
// 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.
// }
// }
Canvas.prototype.getTextInfo = function(text, font, angle) {
if (plot.getOptions().canvas) {
var textStyle, cacheKey, info;
// Cast the value to a string, in case we were given a number
text = "" + text;
// 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 {
textStyle = font;
} }
The top-level "canvas" option controls whether full canvas drawing is enabled, // The text + style + angle uniquely identify the text's
making it easy to toggle on and off. // dimensions and content; we'll use them to build this entry's
// text cache key.
cacheKey = text + "-" + textStyle + "-" + angle;
info = this._textCache[cacheKey] || this._activeTextCache[cacheKey];
By default the plugin extracts font settings from the same CSS styles that the if (info == null) {
default HTML text implementation uses. If *.tickLabel* has a *font-size* of
20px, then the canvas text will be drawn at the same size.
One can also use the "font" option to control these properties directly. The var context = this.context;
format of the font spec object is as follows:
{ // If the font was provided as CSS, create a div with those
size: 11, // classes and examine it to generate a canvas font spec.
style: "italic",
weight: "bold", if (typeof font !== "object") {
family: "sans-serif",
variant: "small-caps" var element;
if (typeof font === "string") {
element = $("<div class='" + font + "'>" + text + "</div>")
.appendTo(this.container);
} else {
element = $("<div>" + text + "</div>")
.appendTo(this.container);
} }
*/ 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")
};
(function($) { textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family;
var options = { element.remove();
canvas: true, }
xaxis: {
font: null // 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
}, },
yaxis: { dimensions: {
font: null width: 0,
height: 0
} }
}; };
function init(plot) { context.save();
context.font = textStyle;
// 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 lines = (text + "").replace(/<br ?\/?>|\r\n|\r/g, "\n").split("\n");
for (var i = 0; i < lines.length; ++i) {
var lineText = lines[i],
measured = context.measureText(lineText),
lineWidth, lineHeight;
lineWidth = measured.width;
// Height might not be defined; not in the standard yet
lineHeight = measured.height || font.size;
// 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.
lineHeight += Math.round(font.size * 0.15);
info.dimensions.width = Math.max(lineWidth, info.dimensions.width);
info.dimensions.height += lineHeight;
info.lines.push({
text: lineText,
width: lineWidth,
height: lineHeight
});
}
context.restore;
}
// 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;
} else {
return getTextInfo.call(this, text, font, angle);
}
}
// Draws a text string onto the canvas.
//
// 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),
dimensions = info.dimensions,
context = this.context,
lines = info.lines;
// Apply alignment to the vertical position of the entire text
if (valign == "middle") {
y -= dimensions.height / 2;
} else if (valign == "bottom") {
y -= dimensions.height;
}
context.save();
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 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
// 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)) {
linex = Math.floor(linex);
y = Math.ceil(y - 2);
}
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({
......
...@@ -34,6 +34,12 @@ Licensed under the MIT license. ...@@ -34,6 +34,12 @@ Licensed under the MIT license.
// the actual Flot code // the actual Flot code
(function($) { (function($) {
// Add default styles for tick labels and other text
$(function() {
$("head").prepend("<style id='flot-default-styles'>.flot-tick-label {font-size:smaller;color:#545454;}</style>");
});
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// The Canvas object is a wrapper around an HTML5 <canvas> tag. // The Canvas object is a wrapper around an HTML5 <canvas> tag.
// //
...@@ -1091,7 +1097,7 @@ Licensed under the MIT license. ...@@ -1091,7 +1097,7 @@ Licensed under the MIT license.
// then whack any remaining obvious garbage left // then whack any remaining obvious garbage left
eventHolder.unbind(); eventHolder.unbind();
placeholder.children().not([surface.element, overlay.element]).remove(); placeholder.children().not([surface.element, surface.text, overlay.element, overlay.text]).remove();
} }
// save in case we get replotted // save in case we get replotted
...@@ -1163,53 +1169,26 @@ Licensed under the MIT license. ...@@ -1163,53 +1169,26 @@ Licensed under the MIT license.
} }
function measureTickLabels(axis) { function measureTickLabels(axis) {
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,
f = axis.font; font = axis.font || "flot-tick-label flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis";
ctx.save();
ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px '" + f.family + "'";
for (var i = 0; i < ticks.length; ++i) { for (var i = 0; i < ticks.length; ++i) {
var t = ticks[i];
t.lines = []; var t = ticks[i],
t.width = t.height = 0; dimensions;
if (!t.label) if (!t.label)
continue; continue;
// accept various kinds of newlines, including HTML ones dimensions = surface.getTextInfo(t.label, font).dimensions;
// (you can actually split directly on regexps in Javascript,
// but IE < 9 is unfortunately broken)
var lines = (t.label + "").replace(/<br ?\/?>|\r\n|\r/g, "\n").split("\n");
for (var j = 0; j < lines.length; ++j) {
var line = { text: lines[j] },
m = ctx.measureText(line.text);
line.width = m.width;
// m.height might not be defined, not in the
// standard yet
line.height = m.height != null ? m.height : f.size;
// 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
line.height += Math.round(f.size * 0.15);
t.width = Math.max(line.width, t.width);
t.height += line.height;
t.lines.push(line);
}
if (opts.labelWidth == null) if (opts.labelWidth == null)
axisw = Math.max(axisw, t.width); axisw = Math.max(axisw, dimensions.width);
if (opts.labelHeight == null) if (opts.labelHeight == null)
axish = Math.max(axish, t.height); axish = Math.max(axish, dimensions.height);
} }
ctx.restore();
axis.labelWidth = Math.ceil(axisw); axis.labelWidth = Math.ceil(axisw);
axis.labelHeight = Math.ceil(axish); axis.labelHeight = Math.ceil(axish);
...@@ -1368,7 +1347,7 @@ Licensed under the MIT license. ...@@ -1368,7 +1347,7 @@ Licensed under the MIT license.
}); });
if (showGrid) { if (showGrid) {
// determine from the placeholder the font size ~ height of font ~ 1 em
var fontDefaults = { var fontDefaults = {
style: placeholder.css("font-style"), style: placeholder.css("font-style"),
size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)), size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)),
...@@ -1385,8 +1364,14 @@ Licensed under the MIT license. ...@@ -1385,8 +1364,14 @@ Licensed under the MIT license.
setTicks(axis); setTicks(axis);
snapRangeToTicks(axis, axis.ticks); snapRangeToTicks(axis, axis.ticks);
// find labelWidth/Height for axis // If a font-spec object was provided, use font defaults
// to fill out any unspecified settings.
if (axis.font) {
axis.font = $.extend({}, fontDefaults, axis.options.font); axis.font = $.extend({}, fontDefaults, axis.options.font);
}
// find labelWidth/Height for axis
measureTickLabels(axis); measureTickLabels(axis);
}); });
...@@ -1663,6 +1648,8 @@ Licensed under the MIT license. ...@@ -1663,6 +1648,8 @@ Licensed under the MIT license.
drawGrid(); drawGrid();
drawAxisLabels(); drawAxisLabels();
} }
surface.render();
} }
function extractRange(ranges, coord) { function extractRange(ranges, coord) {
...@@ -1934,74 +1921,44 @@ Licensed under the MIT license. ...@@ -1934,74 +1921,44 @@ Licensed under the MIT license.
} }
function drawAxisLabels() { function drawAxisLabels() {
ctx.save();
$.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, f = axis.font; var box = axis.box,
// placeholder.append('<div style="position:absolute;opacity:0.10;background-color:red;left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width + 'px;height:' + box.height + 'px"></div>') // debug font = axis.font || "flot-tick-label flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis",
tick, x, y, halign, valign;
ctx.fillStyle = axis.options.color;
// Important: Don't use quotes around axis.font.family! Just around single
// font names like 'Times New Roman' that have a space or special character in it.
ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px " + f.family;
ctx.textAlign = "start";
// middle align the labels - top 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
ctx.textBaseline = "middle";
for (var i = 0; i < axis.ticks.length; ++i) { for (var i = 0; i < axis.ticks.length; ++i) {
var tick = axis.ticks[i];
tick = axis.ticks[i];
if (!tick.label || tick.v < axis.min || tick.v > axis.max) if (!tick.label || tick.v < axis.min || tick.v > axis.max)
continue; continue;
var x, y, offset = 0, line;
for (var k = 0; k < tick.lines.length; ++k) {
line = tick.lines[k];
if (axis.direction == "x") { if (axis.direction == "x") {
x = plotOffset.left + axis.p2c(tick.v) - line.width/2; halign = "center";
if (axis.position == "bottom") x = plotOffset.left + axis.p2c(tick.v);
if (axis.position == "bottom") {
y = box.top + box.padding; y = box.top + box.padding;
else } else {
y = box.top + box.height - box.padding - tick.height; y = box.top + box.height - box.padding;
valign = "bottom";
} }
else { } else {
y = plotOffset.top + axis.p2c(tick.v) - tick.height/2; valign = "middle";
if (axis.position == "left") y = plotOffset.top + axis.p2c(tick.v);
x = box.left + box.width - box.padding - line.width; if (axis.position == "left") {
else x = box.left + box.width - box.padding;
halign = "right";
} else {
x = box.left + box.padding; x = box.left + box.padding;
} }
// account for middle aligning and line number
y += line.height/2 + offset;
offset += line.height;
if (!!(window.opera && window.opera.version().split('.')[0] < 12)) {
// FIXME: LEGACY BROWSER FIX
// AFFECTS: Opera < 12.00
// 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
x = Math.floor(x);
y = Math.ceil(y - 2);
}
ctx.fillText(line.text, x, y);
} }
surface.drawText(x, y, tick.label, font, null, halign, valign);
} }
}); });
ctx.restore();
} }
function drawSeries(series) { function drawSeries(series) {
......
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