1 /* Flot plugin for drawing all elements of a plot on the canvas.
3 Copyright (c) 2007-2013 IOLA and Ole Laursen.
4 Licensed under the MIT license.
6 Flot normally produces certain elements, like axis labels and the legend, using
7 HTML elements. This permits greater interactivity and customization, and often
8 looks better, due to cross-browser canvas text inconsistencies and limitations.
10 It can also be desirable to render the plot entirely in canvas, particularly
11 if the goal is to save it as an image, or if Flot is being used in a context
12 where the HTML DOM does not exist, as is the case within Node.js. This plugin
13 switches out Flot's standard drawing operations for canvas-only replacements.
15 Currently the plugin supports only axis labels, but it will eventually allow
16 every element of the plot to be rendered directly to canvas.
18 The plugin supports these options:
24 The "canvas" option controls whether full canvas drawing is enabled, making it
25 possible to toggle on and off. This is useful when a plot uses HTML text in the
26 browser, but needs to redraw with canvas text when exporting as an image.
36 var render, getTextInfo, addText;
38 // Cache the prototype hasOwnProperty for faster access
40 var hasOwnProperty = Object.prototype.hasOwnProperty;
42 function init(plot, classes) {
44 var Canvas = classes.Canvas;
46 // We only want to replace the functions once; the second time around
47 // we would just get our new function back. This whole replacing of
48 // prototype functions is a disaster, and needs to be changed ASAP.
51 getTextInfo = Canvas.prototype.getTextInfo,
52 addText = Canvas.prototype.addText,
53 render = Canvas.prototype.render;
56 // Finishes rendering the canvas, including overlaid text
58 Canvas.prototype.render = function() {
60 if (!plot.getOptions().canvas) {
61 return render.call(this);
64 var context = this.context,
65 cache = this._textCache;
67 // For each text layer, render elements marked as active
70 context.textBaseline = "middle";
72 for (var layerKey in cache) {
73 if (hasOwnProperty.call(cache, layerKey)) {
74 var layerCache = cache[layerKey];
75 for (var styleKey in layerCache) {
76 if (hasOwnProperty.call(layerCache, styleKey)) {
77 var styleCache = layerCache[styleKey],
79 for (var key in styleCache) {
80 if (hasOwnProperty.call(styleCache, key)) {
82 var info = styleCache[key],
83 positions = info.positions,
86 // Since every element at this level of the cache have the
87 // same font and fill styles, we can just change them once
88 // using the values from the first element.
91 context.fillStyle = info.font.color;
92 context.font = info.font.definition;
96 for (var i = 0, position; position = positions[i]; i++) {
97 if (position.active) {
98 for (var j = 0, line; line = position.lines[j]; j++) {
99 context.fillText(lines[j].text, line[0], line[1]);
102 positions.splice(i--, 1);
106 if (positions.length == 0) {
107 delete styleCache[key];
119 // Creates (if necessary) and returns a text info object.
121 // When the canvas option is set, the object looks like this:
124 // width: Width of the text's bounding box.
125 // height: Height of the text's bounding box.
126 // positions: Array of positions at which this text is drawn.
128 // height: Height of this line.
129 // widths: Width of this line.
130 // text: Text on this line.
133 // definition: Canvas font property string.
134 // color: Color of the text.
138 // The positions array contains objects that look like this:
141 // active: Flag indicating whether the text should be visible.
142 // lines: Array of [x, y] coordinates at which to draw the line.
143 // x: X coordinate at which to draw the text.
144 // y: Y coordinate at which to draw the text.
147 Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) {
149 if (!plot.getOptions().canvas) {
150 return getTextInfo.call(this, layer, text, font, angle, width);
153 var textStyle, layerCache, styleCache, info;
155 // Cast the value to a string, in case we were given a number
159 // If the font is a font-spec object, generate a CSS definition
161 if (typeof font === "object") {
162 textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family;
167 // Retrieve (or create) the cache for the text's layer and styles
169 layerCache = this._textCache[layer];
171 if (layerCache == null) {
172 layerCache = this._textCache[layer] = {};
175 styleCache = layerCache[textStyle];
177 if (styleCache == null) {
178 styleCache = layerCache[textStyle] = {};
181 info = styleCache[text];
185 var context = this.context;
187 // If the font was provided as CSS, create a div with those
188 // classes and examine it to generate a canvas font spec.
190 if (typeof font !== "object") {
192 var element = $("<div> </div>")
193 .css("position", "absolute")
194 .addClass(typeof font === "string" ? font : null)
195 .appendTo(this.getTextLayer(layer));
198 lineHeight: element.height(),
199 style: element.css("font-style"),
200 variant: element.css("font-variant"),
201 weight: element.css("font-weight"),
202 family: element.css("font-family"),
203 color: element.css("color")
206 // Setting line-height to 1, without units, sets it equal
207 // to the font-size, even if the font-size is abstract,
208 // like 'smaller'. This enables us to read the real size
209 // via the element's height, working around browsers that
210 // return the literal 'smaller' value.
212 font.size = element.css("line-height", 1).height();
217 textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family;
219 // Create a new info object, initializing the dimensions to
220 // zero so we can count them up line-by-line.
222 info = styleCache[text] = {
228 definition: textStyle,
234 context.font = textStyle;
236 // Canvas can't handle multi-line strings; break on various
237 // newlines, including HTML brs, to build a list of lines.
238 // Note that we could split directly on regexps, but IE < 9 is
239 // broken; revisit when we drop IE 7/8 support.
241 var lines = (text + "").replace(/<br ?\/?>|\r\n|\r/g, "\n").split("\n");
243 for (var i = 0; i < lines.length; ++i) {
245 var lineText = lines[i],
246 measured = context.measureText(lineText);
248 info.width = Math.max(measured.width, info.width);
249 info.height += font.lineHeight;
253 width: measured.width,
254 height: font.lineHeight
264 // Adds a text string to the canvas text overlay.
266 Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) {
268 if (!plot.getOptions().canvas) {
269 return addText.call(this, layer, x, y, text, font, angle, width, halign, valign);
272 var info = this.getTextInfo(layer, text, font, angle, width),
273 positions = info.positions,
276 // Text is drawn with baseline 'middle', which we need to account
277 // for by adding half a line's height to the y position.
279 y += info.height / lines.length / 2;
281 // Tweak the initial y-position to match vertical alignment
283 if (valign == "middle") {
284 y = Math.round(y - info.height / 2);
285 } else if (valign == "bottom") {
286 y = Math.round(y - info.height);
291 // FIXME: LEGACY BROWSER FIX
292 // AFFECTS: Opera < 12.00
294 // Offset the y coordinate, since Opera is off pretty
295 // consistently compared to the other browsers.
297 if (!!(window.opera && window.opera.version().split(".")[0] < 12)) {
301 // Determine whether this text already exists at this position.
302 // If so, mark it for inclusion in the next render pass.
304 for (var i = 0, position; position = positions[i]; i++) {
305 if (position.x == x && position.y == y) {
306 position.active = true;
311 // If the text doesn't exist at this position, create a new entry
320 positions.push(position);
322 // Fill in the x & y positions of each line, adjusting them
323 // individually for horizontal alignment.
325 for (var i = 0, line; line = lines[i]; i++) {
326 if (halign == "center") {
327 position.lines.push([Math.round(x - line.width / 2), y]);
328 } else if (halign == "right") {
329 position.lines.push([Math.round(x - line.width), y]);
331 position.lines.push([Math.round(x), y]);
338 $.plot.plugins.push({