123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538 |
- /** ## jquery.flot.canvaswrapper
- This plugin contains the function for creating and manipulating both the canvas
- layers and svg layers.
- The Canvas object is a wrapper around an HTML5 canvas tag.
- The constructor Canvas(cls, container) takes as parameters cls,
- the list of classes to apply to the canvas adnd the containter,
- element onto which to append the canvas. The canvas operations
- don't work unless the canvas is attached to the DOM.
- ### jquery.canvaswrapper.js API functions
- */
- (function($) {
- var Canvas = function(cls, container) {
- var element = container.getElementsByClassName(cls)[0];
- if (!element) {
- element = document.createElement('canvas');
- element.className = cls;
- element.style.direction = 'ltr';
- element.style.position = 'absolute';
- element.style.left = '0px';
- element.style.top = '0px';
- container.appendChild(element);
- // If HTML5 Canvas isn't available, throw
- if (!element.getContext) {
- throw new Error('Canvas is not available.');
- }
- }
- this.element = element;
- var context = this.context = element.getContext('2d');
- this.pixelRatio = $.plot.browser.getPixelRatio(context);
- // Size the canvas to match the internal dimensions of its container
- var box = container.getBoundingClientRect();
- this.resize(box.width, box.height);
- // Collection of HTML div layers for text overlaid onto the canvas
- this.SVGContainer = null;
- this.SVG = {};
- // Cache of text fragments and metrics, so we can avoid expensively
- // re-calculating them when the plot is re-rendered in a loop.
- this._textCache = {};
- }
- /**
- - resize(width, height)
- Resizes the canvas to the given dimensions.
- The width represents the new width of the canvas, meanwhile the height
- is the new height of the canvas, both of them in pixels.
- */
- Canvas.prototype.resize = function(width, height) {
- var minSize = 10;
- width = width < minSize ? minSize : width;
- height = height < minSize ? minSize : height;
- var element = this.element,
- context = this.context,
- pixelRatio = this.pixelRatio;
- // Resize the canvas, increasing its density based on the display's
- // pixel ratio; basically giving it more pixels without increasing the
- // size of its element, to take advantage of the fact that retina
- // displays have that many more pixels in the same advertised space.
- // Resizing should reset the state (excanvas seems to be buggy though)
- if (this.width !== width) {
- element.width = width * pixelRatio;
- element.style.width = width + 'px';
- this.width = width;
- }
- if (this.height !== height) {
- element.height = height * pixelRatio;
- element.style.height = height + 'px';
- this.height = height;
- }
- // Save the context, so we can reset in case we get replotted. The
- // restore ensure that we're really back at the initial state, and
- // should be safe even if we haven't saved the initial state yet.
- context.restore();
- context.save();
- // Scale the coordinate space to match the display density; so even though we
- // may have twice as many pixels, we still want lines and other drawing to
- // appear at the same size; the extra pixels will just make them crisper.
- context.scale(pixelRatio, pixelRatio);
- };
- /**
- - clear()
- Clears the entire canvas area, not including any overlaid HTML text
- */
- Canvas.prototype.clear = function() {
- this.context.clearRect(0, 0, this.width, this.height);
- };
- /**
- - render()
- Finishes rendering the canvas, including managing the text overlay.
- */
- Canvas.prototype.render = function() {
- var cache = this._textCache;
- // For each text layer, add elements marked as active that haven't
- // already been rendered, and remove those that are no longer active.
- for (var layerKey in cache) {
- if (hasOwnProperty.call(cache, layerKey)) {
- var layer = this.getSVGLayer(layerKey),
- layerCache = cache[layerKey];
- var display = layer.style.display;
- layer.style.display = 'none';
- for (var styleKey in layerCache) {
- if (hasOwnProperty.call(layerCache, styleKey)) {
- var styleCache = layerCache[styleKey];
- for (var key in styleCache) {
- if (hasOwnProperty.call(styleCache, key)) {
- var val = styleCache[key],
- positions = val.positions;
- for (var i = 0, position; positions[i]; i++) {
- position = positions[i];
- if (position.active) {
- if (!position.rendered) {
- layer.appendChild(position.element);
- position.rendered = true;
- }
- } else {
- positions.splice(i--, 1);
- if (position.rendered) {
- while (position.element.firstChild) {
- position.element.removeChild(position.element.firstChild);
- }
- position.element.parentNode.removeChild(position.element);
- }
- }
- }
- if (positions.length === 0) {
- if (val.measured) {
- val.measured = false;
- } else {
- delete styleCache[key];
- }
- }
- }
- }
- }
- }
- layer.style.display = display;
- }
- }
- };
- /**
- - getSVGLayer(classes)
- Creates (if necessary) and returns the SVG overlay container.
- The classes string represents the string of space-separated CSS classes
- used to uniquely identify the text layer. It return the svg-layer div.
- */
- Canvas.prototype.getSVGLayer = function(classes) {
- var layer = this.SVG[classes];
- // Create the SVG layer if it doesn't exist
- if (!layer) {
- // Create the svg layer container, if it doesn't exist
- var svgElement;
- if (!this.SVGContainer) {
- this.SVGContainer = document.createElement('div');
- this.SVGContainer.className = 'flot-svg';
- this.SVGContainer.style.position = 'absolute';
- this.SVGContainer.style.top = '0px';
- this.SVGContainer.style.left = '0px';
- this.SVGContainer.style.height = '100%';
- this.SVGContainer.style.width = '100%';
- this.SVGContainer.style.pointerEvents = 'none';
- this.element.parentNode.appendChild(this.SVGContainer);
- svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
- svgElement.style.width = '100%';
- svgElement.style.height = '100%';
- this.SVGContainer.appendChild(svgElement);
- } else {
- svgElement = this.SVGContainer.firstChild;
- }
- layer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
- layer.setAttribute('class', classes);
- layer.style.position = 'absolute';
- layer.style.top = '0px';
- layer.style.left = '0px';
- layer.style.bottom = '0px';
- layer.style.right = '0px';
- svgElement.appendChild(layer);
- this.SVG[classes] = layer;
- }
- return layer;
- };
- /**
- - getTextInfo(layer, text, font, angle, width)
- Creates (if necessary) and returns a text info object.
- The object looks like this:
- ```js
- {
- width //Width of the text's wrapper div.
- height //Height of the text's wrapper div.
- element //The HTML div containing the text.
- positions //Array of positions at which this text is drawn.
- }
- ```
- The positions array contains objects that look like this:
- ```js
- {
- active //Flag indicating whether the text should be visible.
- rendered //Flag indicating whether the text is currently visible.
- element //The HTML div containing the text.
- text //The actual text and is identical with element[0].textContent.
- x //X coordinate at which to draw the text.
- y //Y coordinate at which to draw the text.
- }
- ```
- Each position after the first receives a clone of the original element.
- The idea is that that the width, height, and general 'identity' of the
- text is constant no matter where it is placed; the placements are a
- secondary property.
- Canvas maintains a cache of recently-used text info objects; getTextInfo
- either returns the cached element or creates a new entry.
- The layer parameter is string of space-separated CSS classes uniquely
- identifying the layer containing this text.
- Text is the text string to retrieve info for.
- Font is either a string of space-separated CSS classes or a font-spec object,
- defining the text's font and style.
- Angle is the angle at which to rotate the text, in degrees. Angle is currently unused,
- it will be implemented in the future.
- The last parameter is the Maximum width of the text before it wraps.
- The method returns a text info object.
- */
- Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) {
- var textStyle, layerCache, styleCache, info;
- // Cast the value to a string, in case we were given a number or such
- text = '' + text;
- // If the font is a font-spec object, generate a CSS font definition
- if (typeof font === 'object') {
- textStyle = font.style + ' ' + font.variant + ' ' + font.weight + ' ' + font.size + 'px/' + font.lineHeight + 'px ' + font.family;
- } else {
- textStyle = font;
- }
- // Retrieve (or create) the cache for the text's layer and styles
- layerCache = this._textCache[layer];
- if (layerCache == null) {
- layerCache = this._textCache[layer] = {};
- }
- styleCache = layerCache[textStyle];
- if (styleCache == null) {
- styleCache = layerCache[textStyle] = {};
- }
- var key = generateKey(text);
- info = styleCache[key];
- // If we can't find a matching element in our cache, create a new one
- if (!info) {
- var element = document.createElementNS('http://www.w3.org/2000/svg', 'text');
- if (text.indexOf('<br>') !== -1) {
- addTspanElements(text, element, -9999);
- } else {
- var textNode = document.createTextNode(text);
- element.appendChild(textNode);
- }
- element.style.position = 'absolute';
- element.style.maxWidth = width;
- element.setAttributeNS(null, 'x', -9999);
- element.setAttributeNS(null, 'y', -9999);
- if (typeof font === 'object') {
- element.style.font = textStyle;
- element.style.fill = font.fill;
- } else if (typeof font === 'string') {
- element.setAttribute('class', font);
- }
- this.getSVGLayer(layer).appendChild(element);
- var elementRect = element.getBBox();
- info = styleCache[key] = {
- width: elementRect.width,
- height: elementRect.height,
- measured: true,
- element: element,
- positions: []
- };
- //remove elements from dom
- while (element.firstChild) {
- element.removeChild(element.firstChild);
- }
- element.parentNode.removeChild(element);
- }
- info.measured = true;
- return info;
- };
- /**
- - addText (layer, x, y, text, font, angle, width, halign, valign, transforms)
- Adds a text string to the canvas text overlay.
- The text isn't drawn immediately; it is marked as rendering, which will
- result in its addition to the canvas on the next render pass.
- The layer is string of space-separated CSS classes uniquely
- identifying the layer containing this text.
- X and Y represents the X and Y coordinate at which to draw the text.
- and text is the string to draw
- */
- Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign, transforms) {
- var info = this.getTextInfo(layer, text, font, angle, width),
- positions = info.positions;
- // Tweak the div's position to match the text's alignment
- if (halign === 'center') {
- x -= info.width / 2;
- } else if (halign === 'right') {
- x -= info.width;
- }
- if (valign === 'middle') {
- y -= info.height / 2;
- } else if (valign === 'bottom') {
- y -= info.height;
- }
- y += 0.75 * info.height;
- // Determine whether this text already exists at this position.
- // If so, mark it for inclusion in the next render pass.
- for (var i = 0, position; positions[i]; i++) {
- position = positions[i];
- if (position.x === x && position.y === y && position.text === text) {
- position.active = true;
- return;
- } else if (position.active === false) {
- position.active = true;
- position.text = text;
- if (text.indexOf('<br>') !== -1) {
- y -= 0.25 * info.height;
- addTspanElements(text, position.element, x);
- } else {
- position.element.textContent = text;
- }
- position.element.setAttributeNS(null, 'x', x);
- position.element.setAttributeNS(null, 'y', y);
- position.x = x;
- position.y = y;
- return;
- }
- }
- // If the text doesn't exist at this position, create a new entry
- // For the very first position we'll re-use the original element,
- // while for subsequent ones we'll clone it.
- position = {
- active: true,
- rendered: false,
- element: positions.length ? info.element.cloneNode() : info.element,
- text: text,
- x: x,
- y: y
- };
- positions.push(position);
- if (text.indexOf('<br>') !== -1) {
- y -= 0.25 * info.height;
- addTspanElements(text, position.element, x);
- } else {
- position.element.textContent = text;
- }
- // Move the element to its final position within the container
- position.element.setAttributeNS(null, 'x', x);
- position.element.setAttributeNS(null, 'y', y);
- position.element.style.textAlign = halign;
- if (transforms) {
- transforms.forEach(function(t) {
- info.element.transform.baseVal.appendItem(t);
- });
- }
- };
- var addTspanElements = function(text, element, x) {
- var lines = text.split('<br>'),
- tspan, i, offset;
- for (i = 0; i < lines.length; i++) {
- if (!element.childNodes[i]) {
- tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
- element.appendChild(tspan);
- } else {
- tspan = element.childNodes[i];
- }
- tspan.textContent = lines[i];
- offset = i * 1 + 'em';
- tspan.setAttributeNS(null, 'dy', offset);
- tspan.setAttributeNS(null, 'x', x);
- }
- }
- /**
- - removeText (layer, x, y, text, font, angle)
- The function removes one or more text strings from the canvas text overlay.
- If no parameters are given, all text within the layer is removed.
- Note that the text is not immediately removed; it is simply marked as
- inactive, which will result in its removal on the next render pass.
- This avoids the performance penalty for 'clear and redraw' behavior,
- where we potentially get rid of all text on a layer, but will likely
- add back most or all of it later, as when redrawing axes, for example.
- The layer is a string of space-separated CSS classes uniquely
- identifying the layer containing this text. The following parameter are
- X and Y coordinate of the text.
- Text is the string to remove, while the font is either a string of space-separated CSS
- classes or a font-spec object, defining the text's font and style.
- */
- Canvas.prototype.removeText = function(layer, x, y, text, font, angle) {
- var info, htmlYCoord;
- if (text == null) {
- var layerCache = this._textCache[layer];
- if (layerCache != null) {
- for (var styleKey in layerCache) {
- if (hasOwnProperty.call(layerCache, styleKey)) {
- var styleCache = layerCache[styleKey];
- for (var key in styleCache) {
- if (hasOwnProperty.call(styleCache, key)) {
- var positions = styleCache[key].positions;
- positions.forEach(function(position) {
- position.active = false;
- });
- }
- }
- }
- }
- }
- } else {
- info = this.getTextInfo(layer, text, font, angle);
- positions = info.positions;
- positions.forEach(function(position) {
- htmlYCoord = y + 0.75 * info.height;
- if (position.x === x && position.y === htmlYCoord && position.text === text) {
- position.active = false;
- }
- });
- }
- };
- /**
- - clearCache()
- Clears the cache used to speed up the text size measurements.
- As an (unfortunate) side effect all text within the text Layer is removed.
- Use this function before plot.setupGrid() and plot.draw() if the plot just
- became visible or the styles changed.
- */
- Canvas.prototype.clearCache = function() {
- var cache = this._textCache;
- for (var layerKey in cache) {
- if (hasOwnProperty.call(cache, layerKey)) {
- var layer = this.getSVGLayer(layerKey);
- while (layer.firstChild) {
- layer.removeChild(layer.firstChild);
- }
- }
- };
- this._textCache = {};
- };
- function generateKey(text) {
- return text.replace(/0|1|2|3|4|5|6|7|8|9/g, '0');
- }
- if (!window.Flot) {
- window.Flot = {};
- }
- window.Flot.Canvas = Canvas;
- })(jQuery);
|