1/* Flot plugin for drawing all elements of a plot on the canvas. 2 3Copyright (c) 2007-2014 IOLA and Ole Laursen. 4Licensed under the MIT license. 5 6Flot normally produces certain elements, like axis labels and the legend, using 7HTML elements. This permits greater interactivity and customization, and often 8looks better, due to cross-browser canvas text inconsistencies and limitations. 9 10It can also be desirable to render the plot entirely in canvas, particularly 11if the goal is to save it as an image, or if Flot is being used in a context 12where the HTML DOM does not exist, as is the case within Node.js. This plugin 13switches out Flot's standard drawing operations for canvas-only replacements. 14 15Currently the plugin supports only axis labels, but it will eventually allow 16every element of the plot to be rendered directly to canvas. 17 18The plugin supports these options: 19 20{ 21 canvas: boolean 22} 23 24The "canvas" option controls whether full canvas drawing is enabled, making it 25possible to toggle on and off. This is useful when a plot uses HTML text in the 26browser, but needs to redraw with canvas text when exporting as an image. 27 28*/ 29 30(function($) { 31 32 var options = { 33 canvas: true 34 }; 35 36 var render, getTextInfo, addText; 37 38 // Cache the prototype hasOwnProperty for faster access 39 40 var hasOwnProperty = Object.prototype.hasOwnProperty; 41 42 function init(plot, classes) { 43 44 var Canvas = classes.Canvas; 45 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. 49 50 if (render == null) { 51 getTextInfo = Canvas.prototype.getTextInfo, 52 addText = Canvas.prototype.addText, 53 render = Canvas.prototype.render; 54 } 55 56 // Finishes rendering the canvas, including overlaid text 57 58 Canvas.prototype.render = function() { 59 60 if (!plot.getOptions().canvas) { 61 return render.call(this); 62 } 63 64 var context = this.context, 65 cache = this._textCache; 66 67 // For each text layer, render elements marked as active 68 69 context.save(); 70 context.textBaseline = "middle"; 71 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], 78 updateStyles = true; 79 for (var key in styleCache) { 80 if (hasOwnProperty.call(styleCache, key)) { 81 82 var info = styleCache[key], 83 positions = info.positions, 84 lines = info.lines; 85 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. 89 90 if (updateStyles) { 91 context.fillStyle = info.font.color; 92 context.font = info.font.definition; 93 updateStyles = false; 94 } 95 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]); 100 } 101 } else { 102 positions.splice(i--, 1); 103 } 104 } 105 106 if (positions.length == 0) { 107 delete styleCache[key]; 108 } 109 } 110 } 111 } 112 } 113 } 114 } 115 116 context.restore(); 117 }; 118 119 // Creates (if necessary) and returns a text info object. 120 // 121 // When the canvas option is set, the object looks like this: 122 // 123 // { 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. 127 // lines: [{ 128 // height: Height of this line. 129 // widths: Width of this line. 130 // text: Text on this line. 131 // }], 132 // font: { 133 // definition: Canvas font property string. 134 // color: Color of the text. 135 // }, 136 // } 137 // 138 // The positions array contains objects that look like this: 139 // 140 // { 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. 145 // } 146 147 Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { 148 149 if (!plot.getOptions().canvas) { 150 return getTextInfo.call(this, layer, text, font, angle, width); 151 } 152 153 var textStyle, layerCache, styleCache, info; 154 155 // Cast the value to a string, in case we were given a number 156 157 text = "" + text; 158 159 // If the font is a font-spec object, generate a CSS definition 160 161 if (typeof font === "object") { 162 textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; 163 } else { 164 textStyle = font; 165 } 166 167 // Retrieve (or create) the cache for the text's layer and styles 168 169 layerCache = this._textCache[layer]; 170 171 if (layerCache == null) { 172 layerCache = this._textCache[layer] = {}; 173 } 174 175 styleCache = layerCache[textStyle]; 176 177 if (styleCache == null) { 178 styleCache = layerCache[textStyle] = {}; 179 } 180 181 info = styleCache[text]; 182 183 if (info == null) { 184 185 var context = this.context; 186 187 // If the font was provided as CSS, create a div with those 188 // classes and examine it to generate a canvas font spec. 189 190 if (typeof font !== "object") { 191 192 var element = $("<div> </div>") 193 .css("position", "absolute") 194 .addClass(typeof font === "string" ? font : null) 195 .appendTo(this.getTextLayer(layer)); 196 197 font = { 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") 204 }; 205 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. 211 212 font.size = element.css("line-height", 1).height(); 213 214 element.remove(); 215 } 216 217 textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; 218 219 // Create a new info object, initializing the dimensions to 220 // zero so we can count them up line-by-line. 221 222 info = styleCache[text] = { 223 width: 0, 224 height: 0, 225 positions: [], 226 lines: [], 227 font: { 228 definition: textStyle, 229 color: font.color 230 } 231 }; 232 233 context.save(); 234 context.font = textStyle; 235 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. 240 241 var lines = (text + "").replace(/<br ?\/?>|\r\n|\r/g, "\n").split("\n"); 242 243 for (var i = 0; i < lines.length; ++i) { 244 245 var lineText = lines[i], 246 measured = context.measureText(lineText); 247 248 info.width = Math.max(measured.width, info.width); 249 info.height += font.lineHeight; 250 251 info.lines.push({ 252 text: lineText, 253 width: measured.width, 254 height: font.lineHeight 255 }); 256 } 257 258 context.restore(); 259 } 260 261 return info; 262 }; 263 264 // Adds a text string to the canvas text overlay. 265 266 Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { 267 268 if (!plot.getOptions().canvas) { 269 return addText.call(this, layer, x, y, text, font, angle, width, halign, valign); 270 } 271 272 var info = this.getTextInfo(layer, text, font, angle, width), 273 positions = info.positions, 274 lines = info.lines; 275 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. 278 279 y += info.height / lines.length / 2; 280 281 // Tweak the initial y-position to match vertical alignment 282 283 if (valign == "middle") { 284 y = Math.round(y - info.height / 2); 285 } else if (valign == "bottom") { 286 y = Math.round(y - info.height); 287 } else { 288 y = Math.round(y); 289 } 290 291 // FIXME: LEGACY BROWSER FIX 292 // AFFECTS: Opera < 12.00 293 294 // Offset the y coordinate, since Opera is off pretty 295 // consistently compared to the other browsers. 296 297 if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { 298 y -= 2; 299 } 300 301 // Determine whether this text already exists at this position. 302 // If so, mark it for inclusion in the next render pass. 303 304 for (var i = 0, position; position = positions[i]; i++) { 305 if (position.x == x && position.y == y) { 306 position.active = true; 307 return; 308 } 309 } 310 311 // If the text doesn't exist at this position, create a new entry 312 313 position = { 314 active: true, 315 lines: [], 316 x: x, 317 y: y 318 }; 319 320 positions.push(position); 321 322 // Fill in the x & y positions of each line, adjusting them 323 // individually for horizontal alignment. 324 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]); 330 } else { 331 position.lines.push([Math.round(x), y]); 332 } 333 y += line.height; 334 } 335 }; 336 } 337 338 $.plot.plugins.push({ 339 init: init, 340 options: options, 341 name: "canvas", 342 version: "1.0" 343 }); 344 345})(jQuery); 346