From a0529ee8b18d1b6dfe2079f528f999c3451252ff Mon Sep 17 00:00:00 2001 From: David Schnur Date: Fri, 18 Jan 2013 23:26:56 -0500 Subject: [PATCH] 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. --- jquery.flot.canvas.js | 262 +++++++++++++++++++++++++++++++++++++----- jquery.flot.js | 137 ++++++++-------------- 2 files changed, 280 insertions(+), 119 deletions(-) diff --git a/jquery.flot.canvas.js b/jquery.flot.canvas.js index 8a00683..36e1873 100644 --- a/jquery.flot.canvas.js +++ b/jquery.flot.canvas.js @@ -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 top-level "canvas" option controls whether full canvas drawing is enabled, -making it easy to toggle on and off. - -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" - } +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, - xaxis: { - font: null - }, - yaxis: { - font: null - } + canvas: true }; - 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 = $("
" + text + "
") + .appendTo(this.container); + } else { + element = $("
" + text + "
") + .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(/
|\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({ diff --git a/jquery.flot.js b/jquery.flot.js index d6f3306..6571598 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -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(""); + }); + /////////////////////////////////////////////////////////////////////////// // The Canvas object is a wrapper around an HTML5 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(/
|\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); + // 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 - axis.font = $.extend({}, fontDefaults, axis.options.font); 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('
') // 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") - y = box.top + box.padding; - 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; + if (axis.direction == "x") { + 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; + valign = "bottom"; } - - // 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); + } 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; } - ctx.fillText(line.text, x, y); } + + surface.drawText(x, y, tick.label, font, null, halign, valign); } }); - - ctx.restore(); } function drawSeries(series) {