OLD | NEW |
(Empty) | |
| 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 a
round |
| 47 // we would just get our new function back. This whole replacin
g of |
| 48 // prototype functions is a disaster, and needs to be changed AS
AP. |
| 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(layerCac
he, styleKey)) { |
| 77 var styleCache = layerCa
che[styleKey], |
| 78 updateStyles = t
rue; |
| 79 for (var key in styleCac
he) { |
| 80 if (hasOwnProper
ty.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 (upda
teStyles) { |
| 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 (posi
tions.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 visibl
e. |
| 142 // lines: Array of [x, y] coordinates at which to draw the l
ine. |
| 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 n
umber |
| 156 |
| 157 text = "" + text; |
| 158 |
| 159 // If the font is a font-spec object, generate a CSS def
inition |
| 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 a
nd 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 f
ont spec. |
| 189 |
| 190 if (typeof font !== "object") { |
| 191 |
| 192 var element = $("<div> </div>") |
| 193 .css("position", "absolute") |
| 194 .addClass(typeof font === "strin
g" ? font : null) |
| 195 .appendTo(this.getTextLayer(laye
r)); |
| 196 |
| 197 font = { |
| 198 lineHeight: element.height(), |
| 199 style: element.css("font-style")
, |
| 200 variant: element.css("font-varia
nt"), |
| 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 uni
ts, sets it equal |
| 207 // to the font-size, even if the font-si
ze is abstract, |
| 208 // like 'smaller'. This enables us to r
ead the real size |
| 209 // via the element's height, working aro
und 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 di
mensions 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(l
ineText); |
| 247 |
| 248 info.width = Math.max(measured.width, in
fo.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, ang
le, width, halign, valign) { |
| 267 |
| 268 if (!plot.getOptions().canvas) { |
| 269 return addText.call(this, layer, x, y, text, fon
t, angle, width, halign, valign); |
| 270 } |
| 271 |
| 272 var info = this.getTextInfo(layer, text, font, angle, wi
dth), |
| 273 positions = info.positions, |
| 274 lines = info.lines; |
| 275 |
| 276 // Text is drawn with baseline 'middle', which we need t
o 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 alignm
ent |
| 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 po
sition. |
| 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 t
hem |
| 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); |
OLD | NEW |