Details | Last modification | View Log | RSS feed
| Rev | Author | Line No. | Line |
|---|---|---|---|
| 1 | espaco | 1 | /* Flot plugin for drawing all elements of a plot on the canvas. |
| 2 | |||
| 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. |
||
| 4 | Licensed under the MIT license. |
||
| 5 | |||
| 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. |
||
| 9 | |||
| 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. |
||
| 14 | |||
| 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. |
||
| 17 | |||
| 18 | The plugin supports these options: |
||
| 19 | |||
| 20 | { |
||
| 21 | canvas: boolean |
||
| 22 | } |
||
| 23 | |||
| 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. |
||
| 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); |