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 top-level "canvas" option controls whether full canvas drawing is enabled, The "canvas" option controls whether full canvas drawing is enabled, making it
making it easy to toggle on and off. 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.
By default the plugin extracts font settings from the same CSS styles that the
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
format of the font spec object is as follows:
{
size: 11,
style: "italic",
weight: "bold",
family: "sans-serif",
variant: "small-caps"
}
*/ */
(function($) { (function($) {
var options = { var options = {
canvas: true, canvas: true
xaxis: {
font: null
},
yaxis: {
font: null
}
}; };
function init(plot) { 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 text + style + angle uniquely identify the text's
// 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];
if (info == null) {
var context = this.context;
// 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") {
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")
};
textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family;
element.remove();
}
// 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;
// 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);
// 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);
}
// find labelWidth/Height for axis // find labelWidth/Height for axis
axis.font = $.extend({}, fontDefaults, axis.options.font);
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; if (axis.direction == "x") {
for (var k = 0; k < tick.lines.length; ++k) { halign = "center";
line = tick.lines[k]; x = plotOffset.left + axis.p2c(tick.v);
if (axis.position == "bottom") {
if (axis.direction == "x") { y = box.top + box.padding;
x = plotOffset.left + axis.p2c(tick.v) - line.width/2; } else {
if (axis.position == "bottom") y = box.top + box.height - box.padding;
y = box.top + box.padding; valign = "bottom";
else
y = box.top + box.height - box.padding - tick.height;
}
else {
y = plotOffset.top + axis.p2c(tick.v) - tick.height/2;
if (axis.position == "left")
x = box.left + box.width - box.padding - line.width;
else
x = box.left + box.padding;
} }
} else {
// account for middle aligning and line number valign = "middle";
y += line.height/2 + offset; y = plotOffset.top + axis.p2c(tick.v);
offset += line.height; if (axis.position == "left") {
x = box.left + box.width - box.padding;
if (!!(window.opera && window.opera.version().split('.')[0] < 12)) { halign = "right";
// FIXME: LEGACY BROWSER FIX } else {
// AFFECTS: Opera < 12.00 x = box.left + box.padding;
// 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