1 | /** ## jquery.flot.canvaswrapper
|
2 |
|
3 | This plugin contains the function for creating and manipulating both the canvas
|
4 | layers and svg layers.
|
5 |
|
6 | The Canvas object is a wrapper around an HTML5 canvas tag.
|
7 | The constructor Canvas(cls, container) takes as parameters cls,
|
8 | the list of classes to apply to the canvas adnd the containter,
|
9 | element onto which to append the canvas. The canvas operations
|
10 | don't work unless the canvas is attached to the DOM.
|
11 |
|
12 | ### jquery.canvaswrapper.js API functions
|
13 | */
|
14 |
|
15 | (function($) {
|
16 | var Canvas = function(cls, container) {
|
17 | var element = container.getElementsByClassName(cls)[0];
|
18 |
|
19 | if (!element) {
|
20 | element = document.createElement('canvas');
|
21 | element.className = cls;
|
22 | element.style.direction = 'ltr';
|
23 | element.style.position = 'absolute';
|
24 | element.style.left = '0px';
|
25 | element.style.top = '0px';
|
26 |
|
27 | container.appendChild(element);
|
28 |
|
29 | // If HTML5 Canvas isn't available, throw
|
30 |
|
31 | if (!element.getContext) {
|
32 | throw new Error('Canvas is not available.');
|
33 | }
|
34 | }
|
35 |
|
36 | this.element = element;
|
37 |
|
38 | var context = this.context = element.getContext('2d');
|
39 | this.pixelRatio = $.plot.browser.getPixelRatio(context);
|
40 |
|
41 | // Size the canvas to match the internal dimensions of its container
|
42 | var width = $(container).width();
|
43 | var height = $(container).height();
|
44 | this.resize(width, height);
|
45 |
|
46 | // Collection of HTML div layers for text overlaid onto the canvas
|
47 |
|
48 | this.SVGContainer = null;
|
49 | this.SVG = {};
|
50 |
|
51 | // Cache of text fragments and metrics, so we can avoid expensively
|
52 | // re-calculating them when the plot is re-rendered in a loop.
|
53 |
|
54 | this._textCache = {};
|
55 | }
|
56 |
|
57 | /**
|
58 | - resize(width, height)
|
59 |
|
60 | Resizes the canvas to the given dimensions.
|
61 | The width represents the new width of the canvas, meanwhile the height
|
62 | is the new height of the canvas, both of them in pixels.
|
63 | */
|
64 |
|
65 | Canvas.prototype.resize = function(width, height) {
|
66 | var minSize = 10;
|
67 | width = width < minSize ? minSize : width;
|
68 | height = height < minSize ? minSize : height;
|
69 |
|
70 | var element = this.element,
|
71 | context = this.context,
|
72 | pixelRatio = this.pixelRatio;
|
73 |
|
74 | // Resize the canvas, increasing its density based on the display's
|
75 | // pixel ratio; basically giving it more pixels without increasing the
|
76 | // size of its element, to take advantage of the fact that retina
|
77 | // displays have that many more pixels in the same advertised space.
|
78 |
|
79 | // Resizing should reset the state (excanvas seems to be buggy though)
|
80 |
|
81 | if (this.width !== width) {
|
82 | element.width = width * pixelRatio;
|
83 | element.style.width = width + 'px';
|
84 | this.width = width;
|
85 | }
|
86 |
|
87 | if (this.height !== height) {
|
88 | element.height = height * pixelRatio;
|
89 | element.style.height = height + 'px';
|
90 | this.height = height;
|
91 | }
|
92 |
|
93 | // Save the context, so we can reset in case we get replotted. The
|
94 | // restore ensure that we're really back at the initial state, and
|
95 | // should be safe even if we haven't saved the initial state yet.
|
96 |
|
97 | context.restore();
|
98 | context.save();
|
99 |
|
100 | // Scale the coordinate space to match the display density; so even though we
|
101 | // may have twice as many pixels, we still want lines and other drawing to
|
102 | // appear at the same size; the extra pixels will just make them crisper.
|
103 |
|
104 | context.scale(pixelRatio, pixelRatio);
|
105 | };
|
106 |
|
107 | /**
|
108 | - clear()
|
109 |
|
110 | Clears the entire canvas area, not including any overlaid HTML text
|
111 | */
|
112 | Canvas.prototype.clear = function() {
|
113 | this.context.clearRect(0, 0, this.width, this.height);
|
114 | };
|
115 |
|
116 | /**
|
117 | - render()
|
118 |
|
119 | Finishes rendering the canvas, including managing the text overlay.
|
120 | */
|
121 | Canvas.prototype.render = function() {
|
122 | var cache = this._textCache;
|
123 |
|
124 | // For each text layer, add elements marked as active that haven't
|
125 | // already been rendered, and remove those that are no longer active.
|
126 |
|
127 | for (var layerKey in cache) {
|
128 | if (hasOwnProperty.call(cache, layerKey)) {
|
129 | var layer = this.getSVGLayer(layerKey),
|
130 | layerCache = cache[layerKey];
|
131 |
|
132 | var display = layer.style.display;
|
133 | layer.style.display = 'none';
|
134 |
|
135 | for (var styleKey in layerCache) {
|
136 | if (hasOwnProperty.call(layerCache, styleKey)) {
|
137 | var styleCache = layerCache[styleKey];
|
138 | for (var key in styleCache) {
|
139 | if (hasOwnProperty.call(styleCache, key)) {
|
140 | var val = styleCache[key],
|
141 | positions = val.positions;
|
142 |
|
143 | for (var i = 0, position; positions[i]; i++) {
|
144 | position = positions[i];
|
145 | if (position.active) {
|
146 | if (!position.rendered) {
|
147 | layer.appendChild(position.element);
|
148 | position.rendered = true;
|
149 | }
|
150 | } else {
|
151 | positions.splice(i--, 1);
|
152 | if (position.rendered) {
|
153 | while (position.element.firstChild) {
|
154 | position.element.removeChild(position.element.firstChild);
|
155 | }
|
156 | position.element.parentNode.removeChild(position.element);
|
157 | }
|
158 | }
|
159 | }
|
160 |
|
161 | if (positions.length === 0) {
|
162 | if (val.measured) {
|
163 | val.measured = false;
|
164 | } else {
|
165 | delete styleCache[key];
|
166 | }
|
167 | }
|
168 | }
|
169 | }
|
170 | }
|
171 | }
|
172 |
|
173 | layer.style.display = display;
|
174 | }
|
175 | }
|
176 | };
|
177 |
|
178 | /**
|
179 | - getSVGLayer(classes)
|
180 |
|
181 | Creates (if necessary) and returns the SVG overlay container.
|
182 | The classes string represents the string of space-separated CSS classes
|
183 | used to uniquely identify the text layer. It return the svg-layer div.
|
184 | */
|
185 | Canvas.prototype.getSVGLayer = function(classes) {
|
186 | var layer = this.SVG[classes];
|
187 |
|
188 | // Create the SVG layer if it doesn't exist
|
189 |
|
190 | if (!layer) {
|
191 | // Create the svg layer container, if it doesn't exist
|
192 |
|
193 | var svgElement;
|
194 |
|
195 | if (!this.SVGContainer) {
|
196 | this.SVGContainer = document.createElement('div');
|
197 | this.SVGContainer.className = 'flot-svg';
|
198 | this.SVGContainer.style.position = 'absolute';
|
199 | this.SVGContainer.style.top = '0px';
|
200 | this.SVGContainer.style.left = '0px';
|
201 | this.SVGContainer.style.height = '100%';
|
202 | this.SVGContainer.style.width = '100%';
|
203 | this.SVGContainer.style.pointerEvents = 'none';
|
204 | this.element.parentNode.appendChild(this.SVGContainer);
|
205 |
|
206 | svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
207 | svgElement.style.width = '100%';
|
208 | svgElement.style.height = '100%';
|
209 |
|
210 | this.SVGContainer.appendChild(svgElement);
|
211 | } else {
|
212 | svgElement = this.SVGContainer.firstChild;
|
213 | }
|
214 |
|
215 | layer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
216 | layer.setAttribute('class', classes);
|
217 | layer.style.position = 'absolute';
|
218 | layer.style.top = '0px';
|
219 | layer.style.left = '0px';
|
220 | layer.style.bottom = '0px';
|
221 | layer.style.right = '0px';
|
222 | svgElement.appendChild(layer);
|
223 | this.SVG[classes] = layer;
|
224 | }
|
225 |
|
226 | return layer;
|
227 | };
|
228 |
|
229 | /**
|
230 | - getTextInfo(layer, text, font, angle, width)
|
231 |
|
232 | Creates (if necessary) and returns a text info object.
|
233 | The object looks like this:
|
234 | ```js
|
235 | {
|
236 | width //Width of the text's wrapper div.
|
237 | height //Height of the text's wrapper div.
|
238 | element //The HTML div containing the text.
|
239 | positions //Array of positions at which this text is drawn.
|
240 | }
|
241 | ```
|
242 | The positions array contains objects that look like this:
|
243 | ```js
|
244 | {
|
245 | active //Flag indicating whether the text should be visible.
|
246 | rendered //Flag indicating whether the text is currently visible.
|
247 | element //The HTML div containing the text.
|
248 | text //The actual text and is identical with element[0].textContent.
|
249 | x //X coordinate at which to draw the text.
|
250 | y //Y coordinate at which to draw the text.
|
251 | }
|
252 | ```
|
253 | Each position after the first receives a clone of the original element.
|
254 | The idea is that that the width, height, and general 'identity' of the
|
255 | text is constant no matter where it is placed; the placements are a
|
256 | secondary property.
|
257 |
|
258 | Canvas maintains a cache of recently-used text info objects; getTextInfo
|
259 | either returns the cached element or creates a new entry.
|
260 |
|
261 | The layer parameter is string of space-separated CSS classes uniquely
|
262 | identifying the layer containing this text.
|
263 | Text is the text string to retrieve info for.
|
264 | Font is either a string of space-separated CSS classes or a font-spec object,
|
265 | defining the text's font and style.
|
266 | Angle is the angle at which to rotate the text, in degrees. Angle is currently unused,
|
267 | it will be implemented in the future.
|
268 | The last parameter is the Maximum width of the text before it wraps.
|
269 | The method returns a text info object.
|
270 | */
|
271 | Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) {
|
272 | var textStyle, layerCache, styleCache, info;
|
273 |
|
274 | // Cast the value to a string, in case we were given a number or such
|
275 |
|
276 | text = '' + text;
|
277 |
|
278 | // If the font is a font-spec object, generate a CSS font definition
|
279 |
|
280 | if (typeof font === 'object') {
|
281 | textStyle = font.style + ' ' + font.variant + ' ' + font.weight + ' ' + font.size + 'px/' + font.lineHeight + 'px ' + font.family;
|
282 | } else {
|
283 | textStyle = font;
|
284 | }
|
285 |
|
286 | // Retrieve (or create) the cache for the text's layer and styles
|
287 |
|
288 | layerCache = this._textCache[layer];
|
289 |
|
290 | if (layerCache == null) {
|
291 | layerCache = this._textCache[layer] = {};
|
292 | }
|
293 |
|
294 | styleCache = layerCache[textStyle];
|
295 |
|
296 | if (styleCache == null) {
|
297 | styleCache = layerCache[textStyle] = {};
|
298 | }
|
299 |
|
300 | var key = generateKey(text);
|
301 | info = styleCache[key];
|
302 |
|
303 | // If we can't find a matching element in our cache, create a new one
|
304 |
|
305 | if (!info) {
|
306 | var element = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
307 | if (text.indexOf('<br>') !== -1) {
|
308 | addTspanElements(text, element, -9999);
|
309 | } else {
|
310 | var textNode = document.createTextNode(text);
|
311 | element.appendChild(textNode);
|
312 | }
|
313 |
|
314 | element.style.position = 'absolute';
|
315 | element.style.maxWidth = width;
|
316 | element.setAttributeNS(null, 'x', -9999);
|
317 | element.setAttributeNS(null, 'y', -9999);
|
318 |
|
319 | if (typeof font === 'object') {
|
320 | element.style.font = textStyle;
|
321 | element.style.fill = font.fill;
|
322 | } else if (typeof font === 'string') {
|
323 | element.setAttribute('class', font);
|
324 | }
|
325 |
|
326 | this.getSVGLayer(layer).appendChild(element);
|
327 | var elementRect = element.getBBox();
|
328 |
|
329 | info = styleCache[key] = {
|
330 | width: elementRect.width,
|
331 | height: elementRect.height,
|
332 | measured: true,
|
333 | element: element,
|
334 | positions: []
|
335 | };
|
336 |
|
337 | //remove elements from dom
|
338 | while (element.firstChild) {
|
339 | element.removeChild(element.firstChild);
|
340 | }
|
341 | element.parentNode.removeChild(element);
|
342 | }
|
343 |
|
344 | info.measured = true;
|
345 | return info;
|
346 | };
|
347 |
|
348 | function updateTransforms (element, transforms) {
|
349 | element.transform.baseVal.clear();
|
350 | if (transforms) {
|
351 | transforms.forEach(function(t) {
|
352 | element.transform.baseVal.appendItem(t);
|
353 | });
|
354 | }
|
355 | }
|
356 |
|
357 | /**
|
358 | - addText (layer, x, y, text, font, angle, width, halign, valign, transforms)
|
359 |
|
360 | Adds a text string to the canvas text overlay.
|
361 | The text isn't drawn immediately; it is marked as rendering, which will
|
362 | result in its addition to the canvas on the next render pass.
|
363 |
|
364 | The layer is string of space-separated CSS classes uniquely
|
365 | identifying the layer containing this text.
|
366 | X and Y represents the X and Y coordinate at which to draw the text.
|
367 | and text is the string to draw
|
368 | */
|
369 | Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign, transforms) {
|
370 | var info = this.getTextInfo(layer, text, font, angle, width),
|
371 | positions = info.positions;
|
372 |
|
373 | // Tweak the div's position to match the text's alignment
|
374 |
|
375 | if (halign === 'center') {
|
376 | x -= info.width / 2;
|
377 | } else if (halign === 'right') {
|
378 | x -= info.width;
|
379 | }
|
380 |
|
381 | if (valign === 'middle') {
|
382 | y -= info.height / 2;
|
383 | } else if (valign === 'bottom') {
|
384 | y -= info.height;
|
385 | }
|
386 |
|
387 | y += 0.75 * info.height;
|
388 |
|
389 |
|
390 | // Determine whether this text already exists at this position.
|
391 | // If so, mark it for inclusion in the next render pass.
|
392 |
|
393 | for (var i = 0, position; positions[i]; i++) {
|
394 | position = positions[i];
|
395 | if (position.x === x && position.y === y && position.text === text) {
|
396 | position.active = true;
|
397 | // update the transforms
|
398 | updateTransforms(position.element, transforms);
|
399 |
|
400 | return;
|
401 | } else if (position.active === false) {
|
402 | position.active = true;
|
403 | position.text = text;
|
404 | if (text.indexOf('<br>') !== -1) {
|
405 | y -= 0.25 * info.height;
|
406 | addTspanElements(text, position.element, x);
|
407 | } else {
|
408 | position.element.textContent = text;
|
409 | }
|
410 | position.element.setAttributeNS(null, 'x', x);
|
411 | position.element.setAttributeNS(null, 'y', y);
|
412 | position.x = x;
|
413 | position.y = y;
|
414 | // update the transforms
|
415 | updateTransforms(position.element, transforms);
|
416 |
|
417 | return;
|
418 | }
|
419 | }
|
420 |
|
421 | // If the text doesn't exist at this position, create a new entry
|
422 |
|
423 | // For the very first position we'll re-use the original element,
|
424 | // while for subsequent ones we'll clone it.
|
425 |
|
426 | position = {
|
427 | active: true,
|
428 | rendered: false,
|
429 | element: positions.length ? info.element.cloneNode() : info.element,
|
430 | text: text,
|
431 | x: x,
|
432 | y: y
|
433 | };
|
434 |
|
435 | positions.push(position);
|
436 |
|
437 | if (text.indexOf('<br>') !== -1) {
|
438 | y -= 0.25 * info.height;
|
439 | addTspanElements(text, position.element, x);
|
440 | } else {
|
441 | position.element.textContent = text;
|
442 | }
|
443 |
|
444 | // Move the element to its final position within the container
|
445 | position.element.setAttributeNS(null, 'x', x);
|
446 | position.element.setAttributeNS(null, 'y', y);
|
447 | position.element.style.textAlign = halign;
|
448 | // update the transforms
|
449 | updateTransforms(position.element, transforms);
|
450 | };
|
451 |
|
452 | var addTspanElements = function(text, element, x) {
|
453 | var lines = text.split('<br>'),
|
454 | tspan, i, offset;
|
455 |
|
456 | for (i = 0; i < lines.length; i++) {
|
457 | if (!element.childNodes[i]) {
|
458 | tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
459 | element.appendChild(tspan);
|
460 | } else {
|
461 | tspan = element.childNodes[i];
|
462 | }
|
463 | tspan.textContent = lines[i];
|
464 | offset = i * 1 + 'em';
|
465 | tspan.setAttributeNS(null, 'dy', offset);
|
466 | tspan.setAttributeNS(null, 'x', x);
|
467 | }
|
468 | }
|
469 |
|
470 | /**
|
471 | - removeText (layer, x, y, text, font, angle)
|
472 |
|
473 | The function removes one or more text strings from the canvas text overlay.
|
474 | If no parameters are given, all text within the layer is removed.
|
475 |
|
476 | Note that the text is not immediately removed; it is simply marked as
|
477 | inactive, which will result in its removal on the next render pass.
|
478 | This avoids the performance penalty for 'clear and redraw' behavior,
|
479 | where we potentially get rid of all text on a layer, but will likely
|
480 | add back most or all of it later, as when redrawing axes, for example.
|
481 |
|
482 | The layer is a string of space-separated CSS classes uniquely
|
483 | identifying the layer containing this text. The following parameter are
|
484 | X and Y coordinate of the text.
|
485 | Text is the string to remove, while the font is either a string of space-separated CSS
|
486 | classes or a font-spec object, defining the text's font and style.
|
487 | */
|
488 | Canvas.prototype.removeText = function(layer, x, y, text, font, angle) {
|
489 | var info, htmlYCoord;
|
490 | if (text == null) {
|
491 | var layerCache = this._textCache[layer];
|
492 | if (layerCache != null) {
|
493 | for (var styleKey in layerCache) {
|
494 | if (hasOwnProperty.call(layerCache, styleKey)) {
|
495 | var styleCache = layerCache[styleKey];
|
496 | for (var key in styleCache) {
|
497 | if (hasOwnProperty.call(styleCache, key)) {
|
498 | var positions = styleCache[key].positions;
|
499 | positions.forEach(function(position) {
|
500 | position.active = false;
|
501 | });
|
502 | }
|
503 | }
|
504 | }
|
505 | }
|
506 | }
|
507 | } else {
|
508 | info = this.getTextInfo(layer, text, font, angle);
|
509 | positions = info.positions;
|
510 | positions.forEach(function(position) {
|
511 | htmlYCoord = y + 0.75 * info.height;
|
512 | if (position.x === x && position.y === htmlYCoord && position.text === text) {
|
513 | position.active = false;
|
514 | }
|
515 | });
|
516 | }
|
517 | };
|
518 |
|
519 | /**
|
520 | - clearCache()
|
521 |
|
522 | Clears the cache used to speed up the text size measurements.
|
523 | As an (unfortunate) side effect all text within the text Layer is removed.
|
524 | Use this function before plot.setupGrid() and plot.draw() if the plot just
|
525 | became visible or the styles changed.
|
526 | */
|
527 | Canvas.prototype.clearCache = function() {
|
528 | var cache = this._textCache;
|
529 | for (var layerKey in cache) {
|
530 | if (hasOwnProperty.call(cache, layerKey)) {
|
531 | var layer = this.getSVGLayer(layerKey);
|
532 | while (layer.firstChild) {
|
533 | layer.removeChild(layer.firstChild);
|
534 | }
|
535 | }
|
536 | };
|
537 |
|
538 | this._textCache = {};
|
539 | };
|
540 |
|
541 | function generateKey(text) {
|
542 | return text.replace(/0|1|2|3|4|5|6|7|8|9/g, '0');
|
543 | }
|
544 |
|
545 | if (!window.Flot) {
|
546 | window.Flot = {};
|
547 | }
|
548 |
|
549 | window.Flot.Canvas = Canvas;
|
550 | })(jQuery);
|