UNPKG

20.3 kBJavaScriptView Raw
1/** ## jquery.flot.canvaswrapper
2
3This plugin contains the function for creating and manipulating both the canvas
4layers and svg layers.
5
6The Canvas object is a wrapper around an HTML5 canvas tag.
7The constructor Canvas(cls, container) takes as parameters cls,
8the list of classes to apply to the canvas adnd the containter,
9element onto which to append the canvas. The canvas operations
10don'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);