|
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); |