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.
The plugin supports these options:
canvas: boolean,
xaxis, yaxis: {
font: null or font spec object
{
canvas: boolean
}
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,
making it easy to toggle on and off.
// 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];
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.
if (info == null) {
One can also use the "font" option to control these properties directly. The
format of the font spec object is as follows:
var context = this.context;
{
size: 11,
style: "italic",
weight: "bold",
family: "sans-serif",
variant: "small-caps"
// 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")
};
(function($) {
textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family;
var options = {
canvas: true,
xaxis: {
font: null
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
},
yaxis: {
font: null
dimensions: {
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({
......
......@@ -34,6 +34,12 @@ Licensed under the MIT license.
// the actual Flot code
(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.
//
......@@ -1091,7 +1097,7 @@ Licensed under the MIT license.
// then whack any remaining obvious garbage left
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
......@@ -1163,53 +1169,26 @@ Licensed under the MIT license.
}
function measureTickLabels(axis) {
var opts = axis.options, ticks = axis.ticks || [],
axisw = opts.labelWidth || 0, axish = opts.labelHeight || 0,
f = axis.font;
ctx.save();
ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px '" + f.family + "'";
font = axis.font || "flot-tick-label flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis";
for (var i = 0; i < ticks.length; ++i) {
var t = ticks[i];
t.lines = [];
t.width = t.height = 0;
var t = ticks[i],
dimensions;
if (!t.label)
continue;
// accept various kinds of newlines, including HTML ones
// (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);
}
dimensions = surface.getTextInfo(t.label, font).dimensions;
if (opts.labelWidth == null)
axisw = Math.max(axisw, t.width);
axisw = Math.max(axisw, dimensions.width);
if (opts.labelHeight == null)
axish = Math.max(axish, t.height);
axish = Math.max(axish, dimensions.height);
}
ctx.restore();
axis.labelWidth = Math.ceil(axisw);
axis.labelHeight = Math.ceil(axish);
......@@ -1368,7 +1347,7 @@ Licensed under the MIT license.
});
if (showGrid) {
// determine from the placeholder the font size ~ height of font ~ 1 em
var fontDefaults = {
style: placeholder.css("font-style"),
size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)),
......@@ -1385,8 +1364,14 @@ Licensed under the MIT license.
setTicks(axis);
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);
}
// find labelWidth/Height for axis
measureTickLabels(axis);
});
......@@ -1663,6 +1648,8 @@ Licensed under the MIT license.
drawGrid();
drawAxisLabels();
}
surface.render();
}
function extractRange(ranges, coord) {
......@@ -1934,74 +1921,44 @@ Licensed under the MIT license.
}
function drawAxisLabels() {
ctx.save();
$.each(allAxes(), function (_, axis) {
if (!axis.show || axis.ticks.length == 0)
return;
var box = axis.box, f = axis.font;
// 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
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";
var box = axis.box,
font = axis.font || "flot-tick-label flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis",
tick, x, y, halign, valign;
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)
continue;
var x, y, offset = 0, line;
for (var k = 0; k < tick.lines.length; ++k) {
line = tick.lines[k];
if (axis.direction == "x") {
x = plotOffset.left + axis.p2c(tick.v) - line.width/2;
if (axis.position == "bottom")
halign = "center";
x = plotOffset.left + axis.p2c(tick.v);
if (axis.position == "bottom") {
y = box.top + box.padding;
else
y = box.top + box.height - box.padding - tick.height;
} else {
y = box.top + box.height - box.padding;
valign = "bottom";
}
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
} else {
valign = "middle";
y = plotOffset.top + axis.p2c(tick.v);
if (axis.position == "left") {
x = box.left + box.width - box.padding;
halign = "right";
} else {
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) {
......
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