From 04eed2613560475852e0b8258f320f8773dd9228 Mon Sep 17 00:00:00 2001 From: Rachel Shen Date: Wed, 23 Oct 2024 08:15:56 -0600 Subject: [PATCH] [7.17] [Canvas] Update kbn/flot to remove table.replace() issue (#195643) (#197344) # Backport This will backport the following commits from `main` to `7.17`: - [[Canvas] Update kbn/flot to remove table.replace() issue (#195643)](https://github.com/elastic/kibana/pull/195643) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) \n\n### Questions ?\nPlease refer to the [Backport tool\ndocumentation](https://github.com/sqren/backport)\n\n\n\nCo-authored-by: Rachel Shen "}},{"url":"https://github.com/elastic/kibana/pull/197330","number":197330,"branch":"8.x","state":"OPEN"},{"url":"https://github.com/elastic/kibana/pull/197336","number":197336,"branch":"8.15","state":"OPEN"}]}] BACKPORT--> --- .../src/flot_charts/jquery_flot.js | 2415 +++++++++-------- 1 file changed, 1345 insertions(+), 1070 deletions(-) diff --git a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot.js b/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot.js index 5252356279e51..50524fd8f4926 100644 --- a/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot.js +++ b/packages/kbn-ui-shared-deps-src/src/flot_charts/jquery_flot.js @@ -1,8 +1,6 @@ /* JavaScript plotting library for jQuery, version 0.8.3. - Copyright (c) 2007-2014 IOLA and Ole Laursen. Licensed under the MIT license. - */ // first an inline dependency, jquery.colorhelpers.js, we inline it here @@ -29,482 +27,602 @@ Licensed under the MIT license. * V. 1.1: Fix error handling so e.g. parsing an empty string does * produce a color rather than just crashing. */ + (function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); // the actual Flot code +/* Javascript plotting library for jQuery, version 0.9.0-alpha. + +Copyright (c) 2007-2013 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ + (function($) { - // Cache the prototype hasOwnProperty for faster access + // A jquery-esque isNumeric method since we currently support 1.4.4 + // and $.isNumeric was introduced on in 1.7 + var isNumeric = $.isNumeric || function(obj) { + return obj - parseFloat( obj ) >= 0; + }; + + /** + * The Canvas object is a wrapper around an HTML5 tag. + * + * @constructor + * @param {string} cls List of classes to apply to the canvas. + * @param {element} container Element onto which to append the canvas. + * + * Requiring a container is a little iffy, but unfortunately canvas + * operations don't work unless the canvas is attached to the DOM. + */ + function Canvas(cls, container) { + + var element = container.children("." + cls)[0]; - var hasOwnProperty = Object.prototype.hasOwnProperty; + if (element == null) { - // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM - // operation produces the same effect as detach, i.e. removing the element - // without touching its jQuery data. + element = document.createElement("canvas"); + element.className = cls; - // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+. + $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) + .appendTo(container); - if (!$.fn.detach) { - $.fn.detach = function() { - return this.each(function() { - if (this.parentNode) { - this.parentNode.removeChild( this ); + // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas + + if (!element.getContext) { + if (window.G_vmlCanvasManager) { + element = window.G_vmlCanvasManager.initElement(element); + } else { + throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); } - }); - }; + } + } + + this.element = element; + + var context = this.context = element.getContext("2d"); + + // Determine the screen's ratio of physical to device-independent + // pixels. This is the ratio between the canvas width that the browser + // advertises and the number of pixels actually present in that space. + + // The iPhone 4, for example, has a device-independent width of 320px, + // but its screen is actually 640px wide. It therefore has a pixel + // ratio of 2, while most normal devices have a ratio of 1. + + var devicePixelRatio = window.devicePixelRatio || 1, + backingStoreRatio = + context.webkitBackingStorePixelRatio || + context.mozBackingStorePixelRatio || + context.msBackingStorePixelRatio || + context.oBackingStorePixelRatio || + context.backingStorePixelRatio || 1; + + this.pixelRatio = devicePixelRatio / backingStoreRatio; + + // Size the canvas to match the internal dimensions of its container + + this.resize(container.width(), container.height()); + + // Collection of HTML div layers for text overlaid onto the canvas + + this.textContainer = null; + this.text = {}; + + // 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 = {}; } - /////////////////////////////////////////////////////////////////////////// - // The Canvas object is a wrapper around an HTML5 tag. - // - // @constructor - // @param {string} cls List of classes to apply to the canvas. - // @param {element} container Element onto which to append the canvas. - // - // Requiring a container is a little iffy, but unfortunately canvas - // operations don't work unless the canvas is attached to the DOM. + /** + * Resizes the canvas to the given dimensions. + * + * @param {number} width New width of the canvas, in pixels. + * @param {number} width New height of the canvas, in pixels. + */ + Canvas.prototype.resize = function(width, height) { + + if (width <= 0 || height <= 0) { + throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + 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); + }; + + /** + * Clears the entire canvas area, not including any overlaid HTML text + */ + Canvas.prototype.clear = function() { + this.context.clearRect(0, 0, this.width, this.height); + }; + + /** + * 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 (Object.prototype.hasOwnProperty.call(cache, layerKey)) { + + var layer = this.getTextLayer(layerKey), + layerCache = cache[layerKey]; + + layer.hide(); + + for (var styleKey in layerCache) { + if (Object.prototype.hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var angleKey in styleCache) { + if (Object.prototype.hasOwnProperty.call(styleCache, angleKey)) { + var angleCache = styleCache[angleKey]; + for (var key in angleCache) { + if (Object.prototype.hasOwnProperty.call(angleCache, key)) { + + var positions = angleCache[key].positions; + + for (var i = 0, position; position = positions[i]; i++) { + if (position.active) { + if (!position.rendered) { + layer.append(position.element); + position.rendered = true; + } + } else { + positions.splice(i--, 1); + if (position.rendered) { + position.element.detach(); + } + } + } + + if (positions.length === 0) { + delete angleCache[key]; + } + } + } + } + } + } + } + + layer.show(); + } + } + }; + + /** + * Creates (if necessary) and returns the text overlay container. + * + * @param {string} classes String of space-separated CSS classes used to + * uniquely identify the text layer. + * @return {object} The jQuery-wrapped text-layer div. + */ + Canvas.prototype.getTextLayer = function(classes) { + + var layer = this.text[classes]; + + // Create the text layer if it doesn't exist + + if (layer == null) { + + // Create the text layer container, if it doesn't exist + + if (this.textContainer == null) { + this.textContainer = $("
") + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, + "font-size": "smaller", + color: "#545454" + }) + .insertAfter(this.element); + } + + layer = this.text[classes] = $("
") + .addClass(classes) + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + }) + .appendTo(this.textContainer); + } - function Canvas(cls, container) { + return layer; + }; - var element = container.children("." + cls)[0]; + /** + * Creates (if necessary) and returns a text info object. + * + * The object looks like this: + * + * { + * width: Width of the text's wrapper div. + * height: Height of the text's wrapper div. + * element: The jQuery-wrapped HTML div containing the text. + * positions: Array of positions at which this text is drawn. + * } + * + * The positions array contains objects that look like this: + * + * { + * active: Flag indicating whether the text should be visible. + * rendered: Flag indicating whether the text is currently visible. + * element: The jQuery-wrapped HTML div containing the text. + * 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. + * + * @param {string} layer A string of space-separated CSS classes uniquely + * identifying the layer containing this text. + * @param {string} text Text string to retrieve info for. + * @param {(string|object)=} font Either a string of space-separated CSS + * classes or a font-spec object, defining the text's font and style. + * @param {number=} angle Angle at which to rotate the text, in degrees. + * @param {number=} width Maximum width of the text before it wraps. + * @return {object} a text info object. + */ + Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { + + var textStyle, layerCache, styleCache, angleCache, info; + + text = "" + text; // Cast to string in case we have a number or such + angle = (360 + (angle || 0)) % 360; // Normalize the angle to 0...359 + + // 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; + } - if (element == null) { + // Retrieve or create the caches for the text's layer, style, and angle - element = document.createElement("canvas"); - element.className = cls; + layerCache = this._textCache[layer]; + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; + } - $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) - .appendTo(container); + styleCache = layerCache[textStyle]; + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } - // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas + angleCache = styleCache[angle]; + if (angleCache == null) { + angleCache = styleCache[angle] = {}; + } - if (!element.getContext) { - if (window.G_vmlCanvasManager) { - element = window.G_vmlCanvasManager.initElement(element); - } else { - throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); - } - } - } + info = angleCache[text]; - this.element = element; + // If we can't find a matching element in our cache, create a new one - var context = this.context = element.getContext("2d"); + if (info == null) { - // Determine the screen's ratio of physical to device-independent - // pixels. This is the ratio between the canvas width that the browser - // advertises and the number of pixels actually present in that space. + var element = $("
").html(text) + .css({ + position: "absolute", + "max-width": width, + top: -9999 + }) + .appendTo(this.getTextLayer(layer)); - // The iPhone 4, for example, has a device-independent width of 320px, - // but its screen is actually 640px wide. It therefore has a pixel - // ratio of 2, while most normal devices have a ratio of 1. + if (typeof font === "object") { + element.css({ + font: textStyle, + color: font.color + }); + } else if (typeof font === "string") { + element.addClass(font); + } + + // Save the original dimensions of the text; we'll modify these + // later to take into account rotation, if there is any. + + var textWidth = element.outerWidth(true), + textHeight = element.outerHeight(true); + + // Apply rotation to the text using CSS3/IE matrix transforms + + // Note how we also set the element's width, as a work-around for + // the way most browsers resize the div on rotate, which may cause + // the contents to wrap differently. The extra +1 is because IE + // rounds the width differently and needs a little extra help. + + if (angle) { + + var radians = angle * Math.PI / 180, + sin = Math.sin(radians), + cos = Math.cos(radians), + a = cos.toFixed(6), // Use fixed-point so these don't + b = (-sin).toFixed(6), // show up in scientific notation + c = sin.toFixed(6), // when we add them to the string + transformRule; + + if ($.support.leadingWhitespace) { + + // The transform origin defaults to '50% 50%', producing + // blurry text on some browsers (Chrome) when the width or + // height happens to be odd, making 50% fractional. Avoid + // this by setting the origin to rounded values. + + var cx = textWidth / 2, + cy = textHeight / 2, + transformOrigin = Math.floor(cx) + "px " + Math.floor(cy) + "px"; + + // Transforms alter the div's appearance without changing + // its origin. This will make it difficult to position it + // later, since we'll be positioning the new bounding box + // with respect to the old origin. We can work around this + // by adding a translation to align the new bounding box's + // top-left corner with the origin, using the same matrix. + + // Rather than examining all four points, we can use the + // angle to figure out in advance which two points are in + // the top-left quadrant; we can then use the x-coordinate + // of the first (left-most) point and the y-coordinate of + // the second (top-most) point as the bounding box corner. + + var x, y; + if (angle < 90) { + x = Math.floor(cx * cos + cy * sin - cx); + y = Math.floor(cx * sin + cy * cos - cy); + } else if (angle < 180) { + x = Math.floor(cy * sin - cx * cos - cx); + y = Math.floor(cx * sin - cy * cos - cy); + } else if (angle < 270) { + x = Math.floor(-cx * cos - cy * sin - cx); + y = Math.floor(-cx * sin - cy * cos - cy); + } else { + x = Math.floor(cx * cos - cy * sin - cx); + y = Math.floor(cy * cos - cx * sin - cy); + } + + transformRule = "matrix(" + a + "," + c + "," + b + "," + a + "," + x + "," + y + ")"; + + element.css({ + width: textWidth + 1, + transform: transformRule, + "-o-transform": transformRule, + "-ms-transform": transformRule, + "-moz-transform": transformRule, + "-webkit-transform": transformRule, + "transform-origin": transformOrigin, + "-o-transform-origin": transformOrigin, + "-ms-transform-origin": transformOrigin, + "-moz-transform-origin": transformOrigin, + "-webkit-transform-origin": transformOrigin + }); - var devicePixelRatio = window.devicePixelRatio || 1, - backingStoreRatio = - context.webkitBackingStorePixelRatio || - context.mozBackingStorePixelRatio || - context.msBackingStorePixelRatio || - context.oBackingStorePixelRatio || - context.backingStorePixelRatio || 1; + } else { - this.pixelRatio = devicePixelRatio / backingStoreRatio; + // The IE7/8 matrix filter produces very ugly aliasing for + // text with a transparent background. Using a solid color + // greatly improves text clarity, although it does result + // in ugly boxes for plots using a non-white background. - // Size the canvas to match the internal dimensions of its container + // TODO: Instead of white use the actual background color? + // This still wouldn't solve the problem when the plot has + // a gradient background, but it would at least help. - this.resize(container.width(), container.height()); + transformRule = "progid:DXImageTransform.Microsoft.Matrix(M11=" + a + ", M12=" + b + ", M21=" + c + ", M22=" + a + ",sizingMethod='auto expand')"; - // Collection of HTML div layers for text overlaid onto the canvas + element.css({ + width: textWidth + 1, + filter: transformRule, + "-ms-filter": transformRule, + "background-color": "#fff" + }); + } - this.textContainer = null; - this.text = {}; + // Compute the final dimensions of the text's bounding box - // Cache of text fragments and metrics, so we can avoid expensively - // re-calculating them when the plot is re-rendered in a loop. + var ac = Math.abs(cos), + as = Math.abs(sin), + originalWidth = textWidth; + textWidth = Math.round(ac * textWidth + as * textHeight); + textHeight = Math.round(as * originalWidth + ac * textHeight); + } - this._textCache = {}; - } + info = angleCache[text] = { + width: textWidth, + height: textHeight, + element: element, + positions: [] + }; - // Resizes the canvas to the given dimensions. - // - // @param {number} width New width of the canvas, in pixels. - // @param {number} width New height of the canvas, in pixels. + element.detach(); + } - Canvas.prototype.resize = function(width, height) { + return info; + }; - if (width <= 0 || height <= 0) { - throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); - } + /** + * 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. + * + * @param {string} layer A string of space-separated CSS classes uniquely + * identifying the layer containing this text. + * @param {number} x X coordinate at which to draw the text. + * @param {number} y Y coordinate at which to draw the text. + * @param {string} text Text string to draw. + * @param {(string|object)=} font Either a string of space-separated CSS + * classes or a font-spec object, defining the text's font and style. + * @param {number=} angle Angle at which to rotate the text, in degrees. + * @param {number=} width Maximum width of the text before it wraps. + * @param {string=} halign Horizontal alignment of the text; either "left", + * "center" or "right". + * @param {string=} valign Vertical alignment of the text; either "top", + * "middle" or "bottom". + */ + Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { + + 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; + } - var element = this.element, - context = this.context, - pixelRatio = this.pixelRatio; + if (valign === "middle") { + y -= info.height / 2; + } else if (valign === "bottom") { + y -= info.height; + } - // 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. + // Determine whether this text already exists at this position. + // If so, mark it for inclusion in the next render pass. - // Resizing should reset the state (excanvas seems to be buggy though) + for (var i = 0, position; position = positions[i]; i++) { + if (position.x === x && position.y === y) { + position.active = true; + return; + } + } - if (this.width != width) { - element.width = width * pixelRatio; - element.style.width = width + "px"; - this.width = width; - } + // If the text doesn't exist at this position, create a new entry - if (this.height != height) { - element.height = height * pixelRatio; - element.style.height = height + "px"; - this.height = height; - } + // For the very first position we'll re-use the original element, + // while for subsequent ones we'll clone it. - // 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. + position = { + active: true, + rendered: false, + element: positions.length ? info.element.clone() : info.element, + x: x, + y: y + }; - context.restore(); - context.save(); + positions.push(position); - // 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. + // Move the element to its final position within the container - context.scale(pixelRatio, pixelRatio); - }; + position.element.css({ + top: Math.round(y), + left: Math.round(x), + "text-align": halign // In case the text wraps + }); + }; - // Clears the entire canvas area, not including any overlaid HTML text - - Canvas.prototype.clear = function() { - this.context.clearRect(0, 0, this.width, this.height); - }; - - // 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.getTextLayer(layerKey), - layerCache = cache[layerKey]; - - layer.hide(); - - 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; - - for (var i = 0, position; position = positions[i]; i++) { - if (position.active) { - if (!position.rendered) { - layer.append(position.element); - position.rendered = true; - } - } else { - positions.splice(i--, 1); - if (position.rendered) { - position.element.detach(); - } - } - } - - if (positions.length == 0) { - delete styleCache[key]; - } - } - } - } - } - - layer.show(); - } - } - }; - - // Creates (if necessary) and returns the text overlay container. - // - // @param {string} classes String of space-separated CSS classes used to - // uniquely identify the text layer. - // @return {object} The jQuery-wrapped text-layer div. - - Canvas.prototype.getTextLayer = function(classes) { - - var layer = this.text[classes]; - - // Create the text layer if it doesn't exist - - if (layer == null) { - - // Create the text layer container, if it doesn't exist - - if (this.textContainer == null) { - this.textContainer = $("
") - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0, - 'font-size': "smaller", - color: "#545454" - }) - .insertAfter(this.element); - } - - layer = this.text[classes] = $("
") - .addClass(classes) - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0 - }) - .appendTo(this.textContainer); - } - - return layer; - }; - - // Creates (if necessary) and returns a text info object. - // - // The object looks like this: - // - // { - // width: Width of the text's wrapper div. - // height: Height of the text's wrapper div. - // element: The jQuery-wrapped HTML div containing the text. - // positions: Array of positions at which this text is drawn. - // } - // - // The positions array contains objects that look like this: - // - // { - // active: Flag indicating whether the text should be visible. - // rendered: Flag indicating whether the text is currently visible. - // element: The jQuery-wrapped HTML div containing the text. - // 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. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {string} text Text string to retrieve info for. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @return {object} 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] = {}; - } - - info = styleCache[text]; - - // If we can't find a matching element in our cache, create a new one - - if (info == null) { - - var element = $("
").text(text) - .css({ - position: "absolute", - 'max-width': width, - top: -9999 - }) - .appendTo(this.getTextLayer(layer)); - - if (typeof font === "object") { - element.css({ - font: textStyle, - color: font.color - }); - } else if (typeof font === "string") { - element.addClass(font); - } - - info = styleCache[text] = { - width: element.outerWidth(true), - height: element.outerHeight(true), - element: element, - positions: [] - }; - - element.detach(); - } - - return info; - }; - - // 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. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number} x X coordinate at which to draw the text. - // @param {number} y Y coordinate at which to draw the text. - // @param {string} text Text string to draw. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @param {string=} halign Horizontal alignment of the text; either "left", - // "center" or "right". - // @param {string=} valign Vertical alignment of the text; either "top", - // "middle" or "bottom". - - Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { - - 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; - } - - // 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; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = true; - 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.clone() : info.element, - x: x, - y: y - }; - - positions.push(position); - - // Move the element to its final position within the container - - position.element.css({ - top: Math.round(y), - left: Math.round(x), - 'text-align': halign // In case the text wraps - }); - }; - - // 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. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number=} x X coordinate of the text. - // @param {number=} y Y coordinate of the text. - // @param {string=} text Text string to remove. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which the text is rotated, in degrees. - // Angle is currently unused, it will be implemented in the future. - - Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { - 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; - for (var i = 0, position; position = positions[i]; i++) { - position.active = false; - } - } - } - } - } - } - } else { - var positions = this.getTextInfo(layer, text, font, angle).positions; - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = false; - } - } - } - }; - - /////////////////////////////////////////////////////////////////////////// - // The top-level container for the entire plot. + /** + * 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. + * + * @param {string} layer A string of space-separated CSS classes uniquely + * identifying the layer containing this text. + * @param {number=} x X coordinate of the text. + * @param {number=} y Y coordinate of the text. + * @param {string=} text Text string to remove. + * @param {(string|object)=} font Either a string of space-separated CSS + * classes or a font-spec object, defining the text's font and style. + * @param {number=} angle Angle at which the text is rotated, in degrees. + * Angle is currently unused, it will be implemented in the future. + */ + Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { + var i, positions, position; + if (text == null) { + var layerCache = this._textCache[layer]; + if (layerCache != null) { + for (var styleKey in layerCache) { + if (Object.prototype.hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var angleKey in styleCache) { + if (Object.prototype.hasOwnProperty.call(styleCache, angleKey)) { + var angleCache = styleCache[angleKey]; + for (var key in angleCache) { + if (Object.prototype.hasOwnProperty.call(angleCache, key)) { + positions = angleCache[key].positions; + for (i = 0; position = positions[i]; i++) { + position.active = false; + } + } + } + } + } + } + } + } + } else { + positions = this.getTextInfo(layer, text, font, angle).positions; + for (i = 0; position = positions[i]; i++) { + if (position.x === x && position.y === y) { + position.active = false; + } + } + } + }; + /** + * The top-level container for the entire plot. + */ function Plot(placeholder, data_, options_, plugins) { // data is on the form: // [ series1, series2 ... ] @@ -517,7 +635,7 @@ Licensed under the MIT license. colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], legend: { show: true, - noColumns: 1, // number of columns in legend table + noColumns: 1, // number of colums in legend table labelFormatter: null, // fn: string -> string labelBoxBorderColor: "#ccc", // border color for the little label boxes container: null, // container (as jQuery object) to put legend in, null means default on top of graph @@ -528,31 +646,45 @@ Licensed under the MIT license. sorted: null // default to no legend sorting }, xaxis: { - show: null, // null = auto-detect, true = always, false = never - position: "bottom", // or "top" - mode: null, // null or "time" - font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } - color: null, // base color, labels, ticks - tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" - transform: null, // null or f: number -> number to transform axis + + show: null, // null = auto-detect, true = always, false = never + position: "bottom", // or "top" + mode: null, // null or "time" + + color: null, // base color, labels, ticks + font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } + + min: null, // min. value to show, null means set automatically + max: null, // max. value to show, null means set automatically + autoscaleMargin: null, // margin in % to add if auto-setting min/max + + transform: null, // null or f: number -> number to transform axis inverseTransform: null, // if transform is set, this should be the inverse function - min: null, // min. value to show, null means set automatically - max: null, // max. value to show, null means set automatically - autoscaleMargin: null, // margin in % to add if auto-setting min/max - ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks - tickFormatter: null, // fn: number -> string - labelWidth: null, // size of tick labels in pixels - labelHeight: null, - reserveSpace: null, // whether to reserve space even if axis isn't shown - tickLength: null, // size in pixels of ticks, or "full" for whole line - alignTicksWithAxis: null, // axis number or null for no sync - tickDecimals: null, // no. of decimals, null means auto - tickSize: null, // number or [number, "unit"] - minTickSize: null // number or [number, "unit"] + + ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks + tickSize: null, // number or [number, "unit"] + minTickSize: null, // number or [number, "unit"] + tickFormatter: null, // fn: number -> string + tickDecimals: null, // no. of decimals, null means auto + + tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" + tickLength: null, // size in pixels of ticks, or "full" for whole line + + tickWidth: null, // width of tick labels in pixels + tickHeight: null, // height of tick labels in pixels + tickFont: null, // null or font-spec object (see font, above) + + label: null, // null or an axis label string + labelFont: null, // null or font-spec object (see font, above) + labelPadding: 2, // spacing between the axis and its label + + reserveSpace: null, // whether to reserve space even if axis isn't shown + alignTicksWithAxis: null // axis number or null for no sync }, yaxis: { + position: "left", // or "right" autoscaleMargin: 0.02, - position: "left" // or "right" + labelPadding: 2 }, xaxes: [], yaxes: [], @@ -563,6 +695,7 @@ Licensed under the MIT license. lineWidth: 2, // in pixels fill: true, fillColor: "#ffffff", + strokeColor: null, symbol: "circle" // or callback }, lines: { @@ -614,26 +747,26 @@ Licensed under the MIT license. }, hooks: {} }, - surface = null, // the canvas for the plot itself - overlay = null, // canvas for interactive stuff on top of plot - eventHolder = null, // jQuery object that events should be bound to - ctx = null, octx = null, - xaxes = [], yaxes = [], - plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, - plotWidth = 0, plotHeight = 0, - hooks = { - processOptions: [], - processRawData: [], - processDatapoints: [], - processOffset: [], - drawBackground: [], - drawSeries: [], - draw: [], - bindEvents: [], - drawOverlay: [], - shutdown: [] - }, - plot = this; + surface = null, // the canvas for the plot itself + overlay = null, // canvas for interactive stuff on top of plot + eventHolder = null, // jQuery object that events should be bound to + ctx = null, octx = null, + xaxes = [], yaxes = [], + plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, + plotWidth = 0, plotHeight = 0, + hooks = { + processOptions: [], + processRawData: [], + processDatapoints: [], + processOffset: [], + drawBackground: [], + drawSeries: [], + draw: [], + bindEvents: [], + drawOverlay: [], + shutdown: [] + }, + plot = this; // public functions plot.setData = setData; @@ -652,10 +785,11 @@ Licensed under the MIT license. }; plot.getData = function () { return series; }; plot.getAxes = function () { - var res = {}, i; + var res = {}; $.each(xaxes.concat(yaxes), function (_, axis) { - if (axis) - res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; + if (axis) { + res[axis.direction + (axis.n !== 1 ? axis.n : "") + "axis"] = axis; + } }); return res; }; @@ -674,26 +808,9 @@ Licensed under the MIT license. }; }; plot.shutdown = shutdown; - plot.destroy = function () { - shutdown(); - placeholder.removeData("plot").empty(); - - series = []; - options = null; - surface = null; - overlay = null; - eventHolder = null; - ctx = null; - octx = null; - xaxes = []; - yaxes = []; - hooks = null; - highlights = []; - plot = null; - }; plot.resize = function () { - var width = placeholder.width(), - height = placeholder.height(); + var width = placeholder.width(), + height = placeholder.height(); surface.resize(width, height); overlay.resize(width, height); }; @@ -713,8 +830,9 @@ Licensed under the MIT license. function executeHooks(hook, args) { args = [plot].concat(args); - for (var i = 0; i < hook.length; ++i) + for (var i = 0; i < hook.length; ++i) { hook[i].apply(this, args); + } } function initPlugins() { @@ -728,8 +846,9 @@ Licensed under the MIT license. for (var i = 0; i < plugins.length; ++i) { var p = plugins[i]; p.init(plot, classes); - if (p.options) + if (p.options) { $.extend(true, options, p.options); + } } } @@ -743,23 +862,29 @@ Licensed under the MIT license. // not expected behavior; avoid it by replacing them here. if (opts && opts.colors) { - options.colors = opts.colors; + options.colors = opts.colors; } - if (options.xaxis.color == null) - options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - if (options.yaxis.color == null) - options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + if (options.xaxis.color == null) { + options.xaxis.color = $.color.parse(options.grid.color).scale("a", 0.22).toString(); + } + if (options.yaxis.color == null) { + options.yaxis.color = $.color.parse(options.grid.color).scale("a", 0.22).toString(); + } - if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility + if (options.xaxis.tickColor == null) { // grid.tickColor for back-compatibility options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; - if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility + } + if (options.yaxis.tickColor == null) { // grid.tickColor for back-compatibility options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; + } - if (options.grid.borderColor == null) + if (options.grid.borderColor == null) { options.grid.borderColor = options.grid.color; - if (options.grid.tickColor == null) - options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + } + if (options.grid.tickColor == null) { + options.grid.tickColor = $.color.parse(options.grid.color).scale("a", 0.22).toString(); + } // Fill in defaults for axis options, including any unspecified // font-spec fields, if a font-spec was provided. @@ -768,16 +893,16 @@ Licensed under the MIT license. // since the rest of the code assumes that they exist. var i, axisOptions, axisCount, - fontSize = placeholder.css("font-size"), - fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, fontDefaults = { style: placeholder.css("font-style"), - size: Math.round(0.8 * fontSizeDefault), + size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)), variant: placeholder.css("font-variant"), weight: placeholder.css("font-weight"), family: placeholder.css("font-family") }; + fontDefaults.lineHeight = fontDefaults.size * 1.15; + axisCount = options.xaxes.length || 1; for (i = 0; i < axisCount; ++i) { @@ -786,17 +911,29 @@ Licensed under the MIT license. axisOptions.tickColor = axisOptions.color; } + // Compatibility with markrcote/flot-axislabels + + if (axisOptions) { + if (!axisOptions.label && axisOptions.axisLabel) { + axisOptions.label = axisOptions.axisLabel; + } + if (!axisOptions.labelPadding && axisOptions.axisLabelPadding) { + axisOptions.labelPadding = axisOptions.axisLabelPadding; + } + } + axisOptions = $.extend(true, {}, options.xaxis, axisOptions); options.xaxes[i] = axisOptions; + fontDefaults.color = axisOptions.color; if (axisOptions.font) { axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } + } + if (axisOptions.tickFont || axisOptions.font) { + axisOptions.tickFont = $.extend({}, axisOptions.font || fontDefaults, axisOptions.tickFont); + } + if (axisOptions.label && (axisOptions.labelFont || axisOptions.font)) { + axisOptions.labelFont = $.extend({}, axisOptions.font || fontDefaults, axisOptions.labelFont); } } @@ -808,72 +945,83 @@ Licensed under the MIT license. axisOptions.tickColor = axisOptions.color; } + // Compatibility with markrcote/flot-axislabels + + if (axisOptions) { + if (!axisOptions.label && axisOptions.axisLabel) { + axisOptions.label = axisOptions.axisLabel; + } + if (!axisOptions.labelPadding && axisOptions.axisLabelPadding) { + axisOptions.labelPadding = axisOptions.axisLabelPadding; + } + } + axisOptions = $.extend(true, {}, options.yaxis, axisOptions); options.yaxes[i] = axisOptions; + fontDefaults.color = axisOptions.color; if (axisOptions.font) { axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } + } + if (axisOptions.tickFont || axisOptions.font) { + axisOptions.tickFont = $.extend({}, axisOptions.font || fontDefaults, axisOptions.tickFont); + } + if (axisOptions.label && (axisOptions.labelFont || axisOptions.font)) { + axisOptions.labelFont = $.extend({}, axisOptions.font || fontDefaults, axisOptions.labelFont); } } // backwards compatibility, to be removed in future - if (options.xaxis.noTicks && options.xaxis.ticks == null) + if (options.xaxis.noTicks && options.xaxis.ticks == null) { options.xaxis.ticks = options.xaxis.noTicks; - if (options.yaxis.noTicks && options.yaxis.ticks == null) + } + if (options.yaxis.noTicks && options.yaxis.ticks == null) { options.yaxis.ticks = options.yaxis.noTicks; + } if (options.x2axis) { options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); options.xaxes[1].position = "top"; - // Override the inherit to allow the axis to auto-scale - if (options.x2axis.min == null) { - options.xaxes[1].min = null; - } - if (options.x2axis.max == null) { - options.xaxes[1].max = null; - } } if (options.y2axis) { options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); options.yaxes[1].position = "right"; - // Override the inherit to allow the axis to auto-scale - if (options.y2axis.min == null) { - options.yaxes[1].min = null; - } - if (options.y2axis.max == null) { - options.yaxes[1].max = null; - } } - if (options.grid.coloredAreas) + if (options.grid.coloredAreas) { options.grid.markings = options.grid.coloredAreas; - if (options.grid.coloredAreasColor) + } + if (options.grid.coloredAreasColor) { options.grid.markingsColor = options.grid.coloredAreasColor; - if (options.lines) + } + if (options.lines) { $.extend(true, options.series.lines, options.lines); - if (options.points) + } + if (options.points) { $.extend(true, options.series.points, options.points); - if (options.bars) + } + if (options.bars) { $.extend(true, options.series.bars, options.bars); - if (options.shadowSize != null) + } + if (options.shadowSize != null) { options.series.shadowSize = options.shadowSize; - if (options.highlightColor != null) + } + if (options.highlightColor != null) { options.series.highlightColor = options.highlightColor; + } // save options on axes for future reference - for (i = 0; i < options.xaxes.length; ++i) + for (i = 0; i < options.xaxes.length; ++i) { getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; - for (i = 0; i < options.yaxes.length; ++i) + } + for (i = 0; i < options.yaxes.length; ++i) { getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; + } // add hooks from options - for (var n in hooks) - if (options.hooks[n] && options.hooks[n].length) + for (var n in hooks) { + if (options.hooks[n] && options.hooks[n].length) { hooks[n] = hooks[n].concat(options.hooks[n]); + } + } executeHooks(hooks.processOptions, [options]); } @@ -896,9 +1044,9 @@ Licensed under the MIT license. $.extend(true, s, d[i]); d[i].data = s.data; - } - else + } else { s.data = d[i]; + } res.push(s); } @@ -907,10 +1055,12 @@ Licensed under the MIT license. function axisNumber(obj, coord) { var a = obj[coord + "axis"]; - if (typeof a == "object") // if we got a real axis, extract number + if (typeof a === "object") { // if we got a real axis, extract number a = a.n; - if (typeof a != "number") + } + if (!isNumeric(a)) { a = 1; // default to first axis + } return a; } @@ -924,20 +1074,24 @@ Licensed under the MIT license. var res = {}, i, axis; for (i = 0; i < xaxes.length; ++i) { axis = xaxes[i]; - if (axis && axis.used) + if (axis && axis.used) { res["x" + axis.n] = axis.c2p(pos.left); + } } for (i = 0; i < yaxes.length; ++i) { axis = yaxes[i]; - if (axis && axis.used) + if (axis && axis.used) { res["y" + axis.n] = axis.c2p(pos.top); + } } - if (res.x1 !== undefined) + if (res.x1 !== undefined) { res.x = res.x1; - if (res.y1 !== undefined) + } + if (res.y1 !== undefined) { res.y = res.y1; + } return res; } @@ -950,8 +1104,9 @@ Licensed under the MIT license. axis = xaxes[i]; if (axis && axis.used) { key = "x" + axis.n; - if (pos[key] == null && axis.n == 1) + if (pos[key] == null && axis.n === 1) { key = "x"; + } if (pos[key] != null) { res.left = axis.p2c(pos[key]); @@ -964,8 +1119,9 @@ Licensed under the MIT license. axis = yaxes[i]; if (axis && axis.used) { key = "y" + axis.n; - if (pos[key] == null && axis.n == 1) + if (pos[key] == null && axis.n === 1) { key = "y"; + } if (pos[key] != null) { res.top = axis.p2c(pos[key]); @@ -978,12 +1134,13 @@ Licensed under the MIT license. } function getOrCreateAxis(axes, number) { - if (!axes[number - 1]) + if (!axes[number - 1]) { axes[number - 1] = { n: number, // save the number for future reference - direction: axes == xaxes ? "x" : "y", - options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) + direction: axes === xaxes ? "x" : "y", + options: $.extend(true, {}, axes === xaxes ? options.xaxis : options.yaxis) }; + } return axes[number - 1]; } @@ -999,7 +1156,7 @@ Licensed under the MIT license. var sc = series[i].color; if (sc != null) { neededColors--; - if (typeof sc == "number" && sc > maxIndex) { + if (isNumeric(sc) && sc > maxIndex) { maxIndex = sc; } } @@ -1030,15 +1187,19 @@ Licensed under the MIT license. // Reset the variation after every few cycles, or else // it will end up producing only white or black colors. - if (i % colorPoolSize == 0 && i) { + if (i % colorPoolSize === 0 && i) { if (variation >= 0) { if (variation < 0.5) { variation = -variation - 0.2; - } else variation = 0; - } else variation = -variation; + } else { + variation = 0; + } + } else { + variation = -variation; + } } - colors[i] = c.scale('rgb', 1 + variation); + colors[i] = c.scale("rgb", 1 + variation); } // Finalize the series options, filling in their colors @@ -1051,20 +1212,22 @@ Licensed under the MIT license. if (s.color == null) { s.color = colors[colori].toString(); ++colori; - } - else if (typeof s.color == "number") + } else if (isNumeric(s.color)) { s.color = colors[s.color].toString(); + } // turn on lines automatically in case nothing is set if (s.lines.show == null) { var v, show = true; - for (v in s) + for (v in s) { if (s[v] && s[v].show) { show = false; break; } - if (show) + } + if (show) { s.lines.show = true; + } } // If nothing was provided for lines.zero, default it to match @@ -1084,15 +1247,15 @@ Licensed under the MIT license. var topSentry = Number.POSITIVE_INFINITY, bottomSentry = Number.NEGATIVE_INFINITY, fakeInfinity = Number.MAX_VALUE, - i, j, k, m, length, - s, points, ps, x, y, axis, val, f, p, - data, format; + i, j, k, m, s, points, ps, val, f, p, data, format; function updateAxis(axis, min, max) { - if (min < axis.datamin && min != -fakeInfinity) + if (min < axis.datamin && min !== -fakeInfinity) { axis.datamin = min; - if (max > axis.datamax && max != fakeInfinity) + } + if (max > axis.datamax && max !== fakeInfinity) { axis.datamax = max; + } } $.each(allAxes(), function (_, axis) { @@ -1105,7 +1268,6 @@ Licensed under the MIT license. for (i = 0; i < series.length; ++i) { s = series[i]; s.datapoints = { points: [] }; - executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); } @@ -1134,8 +1296,9 @@ Licensed under the MIT license. s.datapoints.format = format; } - if (s.datapoints.pointsize != null) + if (s.datapoints.pointsize != null) { continue; // already filled in + } s.datapoints.pointsize = format.length; @@ -1157,20 +1320,23 @@ Licensed under the MIT license. if (f) { if (f.number && val != null) { val = +val; // convert to number - if (isNaN(val)) + if (isNaN(val)) { val = null; - else if (val == Infinity) + } else if (val === Infinity) { val = fakeInfinity; - else if (val == -Infinity) + } else if (val === -Infinity) { val = -fakeInfinity; + } } if (val == null) { - if (f.required) + if (f.required) { nullify = true; + } - if (f.defaultValue != null) + if (f.defaultValue != null) { val = f.defaultValue; + } } } @@ -1184,7 +1350,7 @@ Licensed under the MIT license. if (val != null) { f = format[m]; // extract min/max info - if (f.autoscale !== false) { + if (f.autoscale) { if (f.x) { updateAxis(s.xaxis, val, val); } @@ -1195,18 +1361,18 @@ Licensed under the MIT license. } points[k + m] = null; } - } - else { + } else { // a little bit of line specific stuff that // perhaps shouldn't be here, but lacking // better means... - if (insertSteps && k > 0 - && points[k - ps] != null - && points[k - ps] != points[k] - && points[k - ps + 1] != points[k + 1]) { + if (insertSteps && k > 0 && + points[k - ps] != null && + points[k - ps] !== points[k] && + points[k - ps + 1] !== points[k + 1]) { // copy the point to make room for a middle point - for (m = 0; m < ps; ++m) + for (m = 0; m < ps; ++m) { points[k + ps + m] = points[k + m]; + } // middle point has same y points[k + 1] = points[k - ps + 1]; @@ -1236,26 +1402,32 @@ Licensed under the MIT license. xmax = bottomSentry, ymax = bottomSentry; for (j = 0; j < points.length; j += ps) { - if (points[j] == null) + if (points[j] == null) { continue; + } for (m = 0; m < ps; ++m) { val = points[j + m]; f = format[m]; - if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) + if (!f || f.autoscale === false || val === fakeInfinity || val === -fakeInfinity) { continue; + } if (f.x) { - if (val < xmin) + if (val < xmin) { xmin = val; - if (val > xmax) + } + if (val > xmax) { xmax = val; + } } if (f.y) { - if (val < ymin) + if (val < ymin) { ymin = val; - if (val > ymax) + } + if (val > ymax) { ymax = val; + } } } } @@ -1265,21 +1437,23 @@ Licensed under the MIT license. var delta; switch (s.bars.align) { - case "left": - delta = 0; - break; - case "right": - delta = -s.bars.barWidth; - break; - default: - delta = -s.bars.barWidth / 2; + case "left": + delta = 0; + break; + case "right": + delta = -s.bars.barWidth; + break; + case "center": + delta = -s.bars.barWidth / 2; + break; + default: + throw new Error("Invalid bar alignment: " + s.bars.align); } if (s.bars.horizontal) { ymin += delta; ymax += delta + s.bars.barWidth; - } - else { + } else { xmin += delta; xmax += delta + s.bars.barWidth; } @@ -1290,10 +1464,12 @@ Licensed under the MIT license. } $.each(allAxes(), function (_, axis) { - if (axis.datamin == topSentry) + if (axis.datamin === topSentry) { axis.datamin = null; - if (axis.datamax == bottomSentry) + } + if (axis.datamax === bottomSentry) { axis.datamax = null; + } }); } @@ -1303,12 +1479,11 @@ Licensed under the MIT license. // from a previous plot in this container that we'll try to re-use. placeholder.css("padding", 0) // padding messes up the positioning - .children().filter(function(){ - return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); - }).remove(); + .children(":not(.flot-base,.flot-overlay)").remove(); - if (placeholder.css("position") == 'static') + if (placeholder.css("position") === "static") { placeholder.css("position", "relative"); // for positioning labels and overlay + } surface = new Canvas("flot-base", placeholder); overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features @@ -1346,15 +1521,17 @@ Licensed under the MIT license. eventHolder.bind("mouseleave", onMouseLeave); } - if (options.grid.clickable) + if (options.grid.clickable) { eventHolder.click(onClick); + } executeHooks(hooks.bindEvents, [eventHolder]); } function shutdown() { - if (redrawTimeout) + if (redrawTimeout) { clearTimeout(redrawTimeout); + } eventHolder.unbind("mousemove", onMouseMove); eventHolder.unbind("mouseleave", onMouseLeave); @@ -1374,136 +1551,143 @@ Licensed under the MIT license. // precompute how much the axis is scaling a point // in canvas space - if (axis.direction == "x") { + if (axis.direction === "x") { s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); m = Math.min(t(axis.max), t(axis.min)); - } - else { + } else { s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); s = -s; m = Math.max(t(axis.max), t(axis.min)); } // data point to canvas coordinate - if (t == identity) // slight optimization + if (t === identity) { // slight optimization axis.p2c = function (p) { return (p - m) * s; }; - else + } else { axis.p2c = function (p) { return (t(p) - m) * s; }; + } // canvas coordinate to data point - if (!it) + if (!it) { axis.c2p = function (c) { return m + c / s; }; - else + } else { axis.c2p = function (c) { return it(m + c / s); }; + } } function measureTickLabels(axis) { var opts = axis.options, ticks = axis.ticks || [], - labelWidth = opts.labelWidth || 0, - labelHeight = opts.labelHeight || 0, - maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = opts.font || "flot-tick-label tickLabel"; + // Label width & height are deprecated; remove in 1.0! + tickWidth = opts.tickWidth || opts.labelWidth || 0, + tickHeight = opts.tickHeight || opts.labelHeight || 0, + maxWidth = tickWidth || axis.direction === "x" ? Math.floor(surface.width / (ticks.length || 1)) : null, + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + axis.direction + "Axis " + axis.direction + axis.n + "Axis", + font = opts.tickFont || "flot-tick-label tickLabel"; for (var i = 0; i < ticks.length; ++i) { var t = ticks[i]; - if (!t.label) + if (!t.label) { continue; + } var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); - labelWidth = Math.max(labelWidth, info.width); - labelHeight = Math.max(labelHeight, info.height); + tickWidth = Math.max(tickWidth, info.width); + tickHeight = Math.max(tickHeight, info.height); } - axis.labelWidth = opts.labelWidth || labelWidth; - axis.labelHeight = opts.labelHeight || labelHeight; + axis.tickWidth = opts.tickWidth || opts.labelWidth || tickWidth; + axis.tickHeight = opts.tickHeight || opts.labelHeight || tickHeight; + + // Label width/height properties are deprecated; remove in 1.0! + + axis.labelWidth = axis.tickWidth; + axis.labelHeight = axis.tickHeight; } + /////////////////////////////////////////////////////////////////////// + // Compute the axis bounding box based on the dimensions of its label + // and tick labels, then adjust the plotOffset to make room for it. + // + // This first phase only considers one dimension per axis; the other + // dimension depends on the other axes, and will be calculated later. + function allocateAxisBoxFirstPhase(axis) { - // find the bounding box of the axis by looking at label - // widths/heights and ticks, make room by diminishing the - // plotOffset; this first phase only looks at one - // dimension per axis, the other dimension depends on the - // other axes so will have to wait - - var lw = axis.labelWidth, - lh = axis.labelHeight, - pos = axis.options.position, - isXAxis = axis.direction === "x", - tickLength = axis.options.tickLength, + + var contentWidth = axis.tickWidth, + contentHeight = axis.tickHeight, + axisOptions = axis.options, + tickLength = axisOptions.tickLength, + axisPosition = axisOptions.position, axisMargin = options.grid.axisMargin, padding = options.grid.labelMargin, - innermost = true, - outermost = true, - first = true, - found = false; - - // Determine the axis's position in its direction and on its side - - $.each(isXAxis ? xaxes : yaxes, function(i, a) { - if (a && (a.show || a.reserveSpace)) { - if (a === axis) { - found = true; - } else if (a.options.position === pos) { - if (found) { - outermost = false; - } else { - innermost = false; - } - } - if (!found) { - first = false; - } - } - }); + all = axis.direction === "x" ? xaxes : yaxes, + innermost; - // The outermost axis on each side has no margin + // Determine the margin around the axis - if (outermost) { - axisMargin = 0; + var samePosition = $.grep(all, function(axis) { + return axis && axis.options.position === axisPosition && axis.reserveSpace; + }); + if ($.inArray(axis, samePosition) === samePosition.length - 1) { + axisMargin = 0; // outermost } - // The ticks for the first axis in each direction stretch across + // Determine whether the axis is the first (innermost) on its side + + innermost = $.inArray(axis, samePosition) === 0; + + // Determine the length of the tick marks if (tickLength == null) { - tickLength = first ? "full" : 5; + if (innermost) { + tickLength = "full"; + } else { + tickLength = 5; + } } - if (!isNaN(+tickLength)) + if (!isNaN(+tickLength)) { padding += +tickLength; + } - if (isXAxis) { - lh += padding; + // Measure the dimensions of the axis label, if it has one - if (pos == "bottom") { - plotOffset.bottom += lh + axisMargin; - axis.box = { top: surface.height - plotOffset.bottom, height: lh }; - } - else { - axis.box = { top: plotOffset.top + axisMargin, height: lh }; - plotOffset.top += lh + axisMargin; - } + if (axisOptions.label) { + var layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + axis.direction + "Axis " + axis.direction + axis.n + "Axis", + font = axisOptions.labelFont || "flot-axis-label axisLabels " + axis.direction + axis.n + "axisLabel", + angle = axis.direction === "x" ? 0 : axisOptions.position === "right" ? 90 : -90, + labelInfo = surface.getTextInfo(layer, axisOptions.label, font, angle); + contentWidth += labelInfo.width + axisOptions.labelPadding; + contentHeight += labelInfo.height + axisOptions.labelPadding; } - else { - lw += padding; - if (pos == "left") { - axis.box = { left: plotOffset.left + axisMargin, width: lw }; - plotOffset.left += lw + axisMargin; + // Compute the axis bounding box and update the plot bounds + + if (axis.direction === "x") { + contentHeight += padding; + if (axisPosition === "top") { + axis.box = { top: plotOffset.top + axisMargin, height: contentHeight }; + plotOffset.top += contentHeight + axisMargin; + } else { + plotOffset.bottom += contentHeight + axisMargin; + axis.box = { top: surface.height - plotOffset.bottom, height: contentHeight }; } - else { - plotOffset.right += lw + axisMargin; - axis.box = { left: surface.width - plotOffset.right, width: lw }; + } else { + contentWidth += padding; + if (axisPosition === "right") { + plotOffset.right += contentWidth + axisMargin; + axis.box = { left: surface.width - plotOffset.right, width: contentWidth }; + } else { + axis.box = { left: plotOffset.left + axisMargin, width: contentWidth }; + plotOffset.left += contentWidth + axisMargin; } } - // save for future reference - axis.position = pos; + axis.position = axisPosition; axis.tickLength = tickLength; axis.box.padding = padding; axis.innermost = innermost; @@ -1512,13 +1696,12 @@ Licensed under the MIT license. function allocateAxisBoxSecondPhase(axis) { // now that all axis boxes have been placed in one // dimension, we can set the remaining dimension coordinates - if (axis.direction == "x") { - axis.box.left = plotOffset.left - axis.labelWidth / 2; - axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; - } - else { - axis.box.top = plotOffset.top - axis.labelHeight / 2; - axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; + if (axis.direction === "x") { + axis.box.left = plotOffset.left - axis.tickWidth / 2; + axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.tickWidth; + } else { + axis.box.top = plotOffset.top - axis.tickHeight / 2; + axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.tickHeight; } } @@ -1527,95 +1710,92 @@ Licensed under the MIT license. // inside the canvas and isn't clipped off var minMargin = options.grid.minBorderMargin, - axis, i; + margins = { x: 0, y: 0 }, i; // check stuff from the plot (FIXME: this should just read // a value from the series, otherwise it's impossible to // customize) if (minMargin == null) { minMargin = 0; - for (i = 0; i < series.length; ++i) + for (i = 0; i < series.length; ++i) { minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); + } } - var margins = { - left: minMargin, - right: minMargin, - top: minMargin, - bottom: minMargin - }; + margins.x = margins.y = Math.ceil(minMargin); // check axis labels, note we don't check the actual // labels but instead use the overall width/height to not // jump as much around with replots $.each(allAxes(), function (_, axis) { - if (axis.reserveSpace && axis.ticks && axis.ticks.length) { - if (axis.direction === "x") { - margins.left = Math.max(margins.left, axis.labelWidth / 2); - margins.right = Math.max(margins.right, axis.labelWidth / 2); - } else { - margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); - margins.top = Math.max(margins.top, axis.labelHeight / 2); - } + var dir = axis.direction; + if (axis.reserveSpace) { + margins[dir] = Math.ceil(Math.max(margins[dir], (dir === "x" ? axis.tickWidth : axis.tickHeight) / 2)); } }); - plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); - plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); - plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); - plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); + plotOffset.left = Math.max(margins.x, plotOffset.left); + plotOffset.right = Math.max(margins.x, plotOffset.right); + plotOffset.top = Math.max(margins.y, plotOffset.top); + plotOffset.bottom = Math.max(margins.y, plotOffset.bottom); } function setupGrid() { - var i, axes = allAxes(), showGrid = options.grid.show; + var axes = allAxes(), + showGrid = options.grid.show, + margin = options.grid.margin || 0, + i, a; // Initialize the plot's offset from the edge of the canvas - for (var a in plotOffset) { - var margin = options.grid.margin || 0; - plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; + for (a in plotOffset) { + if (Object.prototype.hasOwnProperty.call(plotOffset, a)) { + plotOffset[a] = isNumeric(margin) ? margin : margin[a] || 0; + } } executeHooks(hooks.processOffset, [plotOffset]); // If the grid is visible, add its border width to the offset - for (var a in plotOffset) { - if(typeof(options.grid.borderWidth) == "object") { + for (a in plotOffset) { + if(typeof(options.grid.borderWidth) === "object") { plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; - } - else { + } else { plotOffset[a] += showGrid ? options.grid.borderWidth : 0; } } + // init axes $.each(axes, function (_, axis) { - var axisOpts = axis.options; - axis.show = axisOpts.show == null ? axis.used : axisOpts.show; - axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; + axis.show = axis.options.show; + if (axis.show == null) { + axis.show = axis.used; // by default an axis is visible if it's got data + } + + axis.reserveSpace = axis.show || axis.options.reserveSpace; + setRange(axis); }); if (showGrid) { - var allocatedAxes = $.grep(axes, function (axis) { - return axis.show || axis.reserveSpace; - }); + var allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; }); $.each(allocatedAxes, function (_, axis) { // make the ticks setupTickGeneration(axis); setTicks(axis); snapRangeToTicks(axis, axis.ticks); - // find labelWidth/Height for axis measureTickLabels(axis); }); // with all dimensions calculated, we can compute the // axis bounding boxes, start from the outside // (reverse order) - for (i = allocatedAxes.length - 1; i >= 0; --i) + for (i = allocatedAxes.length - 1; i >= 0; --i) { allocateAxisBoxFirstPhase(allocatedAxes[i]); + } // make sure we've got enough space for things that // might stick out @@ -1647,18 +1827,19 @@ Licensed under the MIT license. max = +(opts.max != null ? opts.max : axis.datamax), delta = max - min; - if (delta == 0.0) { + if (delta === 0.0) { // degenerate case - var widen = max == 0 ? 1 : 0.01; + var widen = max === 0 ? 1 : 0.01; - if (opts.min == null) + if (opts.min == null) { min -= widen; + } // always widen max if we couldn't widen min to ensure we // don't fall into min == max which doesn't work - if (opts.max == null || opts.min != null) + if (opts.max == null || opts.min != null) { max += widen; - } - else { + } + } else { // consider autoscaling var margin = opts.autoscaleMargin; if (margin != null) { @@ -1666,13 +1847,15 @@ Licensed under the MIT license. min -= delta * margin; // make sure we don't go below zero if all values // are positive - if (min < 0 && axis.datamin != null && axis.datamin >= 0) + if (min < 0 && axis.datamin != null && axis.datamin >= 0) { min = 0; + } } if (opts.max == null) { max += delta * margin; - if (max > 0 && axis.datamax != null && axis.datamax <= 0) + if (max > 0 && axis.datamax != null && axis.datamax <= 0) { max = 0; + } } } } @@ -1685,12 +1868,13 @@ Licensed under the MIT license. // estimate number of ticks var noTicks; - if (typeof opts.ticks == "number" && opts.ticks > 0) + if (isNumeric(opts.ticks) && opts.ticks > 0) { noTicks = opts.ticks; - else + } else { // heuristic based on the model a*sqrt(x) fitted to // some data points that seemed reasonable - noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); + noTicks = 0.3 * Math.sqrt(axis.direction === "x" ? surface.width : surface.height); + } var delta = (axis.max - axis.min) / noTicks, dec = -Math.floor(Math.log(delta) / Math.LN10), @@ -1729,10 +1913,10 @@ Licensed under the MIT license. axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); axis.tickSize = opts.tickSize || size; - // Time mode was moved to a plug-in in 0.8, and since so many people use it - // we'll add an especially friendly reminder to make sure they included it. + // Time mode was moved to a plug-in in 0.8, but since so many people use this + // we'll add an especially friendly make sure they remembered to include it. - if (opts.mode == "time" && !axis.tickGenerator) { + if (opts.mode === "time" && !axis.tickGenerator) { throw new Error("Time mode requires the flot.time plugin."); } @@ -1754,43 +1938,46 @@ Licensed under the MIT license. v = start + i * axis.tickSize; ticks.push(v); ++i; - } while (v < axis.max && v != prev); + } while (v < axis.max && v !== prev); return ticks; }; - axis.tickFormatter = function (value, axis) { + axis.tickFormatter = function (value, axis) { - var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; - var formatted = "" + Math.round(value * factor) / factor; + var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; + var formatted = "" + Math.round(value * factor) / factor; - // If tickDecimals was specified, ensure that we have exactly that - // much precision; otherwise default to the value's own precision. + // If tickDecimals was specified, ensure that we have exactly that + // much precision; otherwise default to the value's own precision. - if (axis.tickDecimals != null) { - var decimal = formatted.indexOf("."); - var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; - if (precision < axis.tickDecimals) { - return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); - } - } + if (axis.tickDecimals != null) { + var decimal = formatted.indexOf("."); + var precision = decimal === -1 ? 0 : formatted.length - decimal - 1; + if (precision < axis.tickDecimals) { + return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); + } + } return formatted; }; } - if ($.isFunction(opts.tickFormatter)) + if ($.isFunction(opts.tickFormatter)) { axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; + } if (opts.alignTicksWithAxis != null) { - var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; - if (otherAxis && otherAxis.used && otherAxis != axis) { + var otherAxis = (axis.direction === "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; + if (otherAxis && otherAxis.used && otherAxis !== axis) { // consider snapping min/max to outermost nice ticks var niceTicks = axis.tickGenerator(axis); if (niceTicks.length > 0) { - if (opts.min == null) + if (opts.min == null) { axis.min = Math.min(axis.min, niceTicks[0]); - if (opts.max == null && niceTicks.length > 1) + } + if (opts.max == null && niceTicks.length > 1) { axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); + } } axis.tickGenerator = function (axis) { @@ -1813,8 +2000,9 @@ Licensed under the MIT license. // only proceed if the tick interval rounded // with an extra decimal doesn't give us a // zero at end - if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) + if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) { axis.tickDecimals = extraDec; + } } } } @@ -1822,14 +2010,15 @@ Licensed under the MIT license. function setTicks(axis) { var oticks = axis.options.ticks, ticks = []; - if (oticks == null || (typeof oticks == "number" && oticks > 0)) + if (oticks == null || (isNumeric(oticks) && oticks > 0)) { ticks = axis.tickGenerator(axis); - else if (oticks) { - if ($.isFunction(oticks)) + } else if (oticks) { + if ($.isFunction(oticks)) { // generate the ticks ticks = oticks(axis); - else + } else { ticks = oticks; + } } // clean up/labelify the supplied ticks, copy them over @@ -1838,27 +2027,32 @@ Licensed under the MIT license. for (i = 0; i < ticks.length; ++i) { var label = null; var t = ticks[i]; - if (typeof t == "object") { + if (typeof t === "object") { v = +t[0]; - if (t.length > 1) + if (t.length > 1) { label = t[1]; - } - else + } + } else { v = +t; - if (label == null) + } + if (label == null) { label = axis.tickFormatter(v, axis); - if (!isNaN(v)) + } + if (!isNaN(v)) { axis.ticks.push({ v: v, label: label }); + } } } function snapRangeToTicks(axis, ticks) { if (axis.options.autoscaleMargin && ticks.length > 0) { // snap to ticks - if (axis.options.min == null) + if (axis.options.min == null) { axis.min = Math.min(axis.min, ticks[0].v); - if (axis.options.max == null && ticks.length > 1) + } + if (axis.options.max == null && ticks.length > 1) { axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); + } } } @@ -1871,8 +2065,9 @@ Licensed under the MIT license. var grid = options.grid; // draw background, if any - if (grid.show && grid.backgroundColor) + if (grid.show && grid.backgroundColor) { drawBackground(); + } if (grid.show && !grid.aboveData) { drawGrid(); @@ -1902,10 +2097,11 @@ Licensed under the MIT license. for (var i = 0; i < axes.length; ++i) { axis = axes[i]; - if (axis.direction == coord) { + if (axis.direction === coord) { key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) + if (!ranges[key] && axis.n === 1) { key = coord + "axis"; // support x1axis as xaxis + } if (ranges[key]) { from = ranges[key].from; to = ranges[key].to; @@ -1916,7 +2112,7 @@ Licensed under the MIT license. // backwards-compat stuff - to be removed in future if (!ranges[key]) { - axis = coord == "x" ? xaxes[0] : yaxes[0]; + axis = coord === "x" ? xaxes[0] : yaxes[0]; from = ranges[coord + "1"]; to = ranges[coord + "2"]; } @@ -1967,53 +2163,50 @@ Licensed under the MIT license. yrange = extractRange(m, "y"); // fill in missing - if (xrange.from == null) + if (xrange.from == null) { xrange.from = xrange.axis.min; - if (xrange.to == null) + } + if (xrange.to == null) { xrange.to = xrange.axis.max; - if (yrange.from == null) + } + if (yrange.from == null) { yrange.from = yrange.axis.min; - if (yrange.to == null) + } + if (yrange.to == null) { yrange.to = yrange.axis.max; + } // clip if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || - yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) + yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) { continue; + } xrange.from = Math.max(xrange.from, xrange.axis.min); xrange.to = Math.min(xrange.to, xrange.axis.max); yrange.from = Math.max(yrange.from, yrange.axis.min); yrange.to = Math.min(yrange.to, yrange.axis.max); - var xequal = xrange.from === xrange.to, - yequal = yrange.from === yrange.to; - - if (xequal && yequal) { + if (xrange.from === xrange.to && yrange.from === yrange.to) { continue; } // then draw - xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); - xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); - yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); - yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); - - if (xequal || yequal) { - var lineWidth = m.lineWidth || options.grid.markingsLineWidth, - subPixel = lineWidth % 2 ? 0.5 : 0; + xrange.from = xrange.axis.p2c(xrange.from); + xrange.to = xrange.axis.p2c(xrange.to); + yrange.from = yrange.axis.p2c(yrange.from); + yrange.to = yrange.axis.p2c(yrange.to); + + if (xrange.from === xrange.to || yrange.from === yrange.to) { + // draw line ctx.beginPath(); ctx.strokeStyle = m.color || options.grid.markingsColor; - ctx.lineWidth = lineWidth; - if (xequal) { - ctx.moveTo(xrange.to + subPixel, yrange.from); - ctx.lineTo(xrange.to + subPixel, yrange.to); - } else { - ctx.moveTo(xrange.from, yrange.to + subPixel); - ctx.lineTo(xrange.to, yrange.to + subPixel); - } + ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth; + ctx.moveTo(xrange.from, yrange.from); + ctx.lineTo(xrange.to, yrange.to); ctx.stroke(); } else { + // fill area ctx.fillStyle = m.color || options.grid.markingsColor; ctx.fillRect(xrange.from, yrange.to, xrange.to - xrange.from, @@ -2029,25 +2222,27 @@ Licensed under the MIT license. for (var j = 0; j < axes.length; ++j) { var axis = axes[j], box = axis.box, t = axis.tickLength, x, y, xoff, yoff; - if (!axis.show || axis.ticks.length == 0) + if (!axis.show || axis.ticks.length === 0) { continue; + } ctx.lineWidth = 1; // find the edges - if (axis.direction == "x") { + if (axis.direction === "x") { x = 0; - if (t == "full") - y = (axis.position == "top" ? 0 : plotHeight); - else - y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); - } - else { + if (t === "full") { + y = (axis.position === "top" ? 0 : plotHeight); + } else { + y = box.top - plotOffset.top + (axis.position === "top" ? box.height : 0); + } + } else { y = 0; - if (t == "full") - x = (axis.position == "left" ? 0 : plotWidth); - else - x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); + if (t === "full") { + x = (axis.position === "left" ? 0 : plotWidth); + } else { + x = box.left - plotOffset.left + (axis.position === "left" ? box.width : 0); + } } // draw tick bar @@ -2055,13 +2250,14 @@ Licensed under the MIT license. ctx.strokeStyle = axis.options.color; ctx.beginPath(); xoff = yoff = 0; - if (axis.direction == "x") + if (axis.direction === "x") { xoff = plotWidth + 1; - else + } else { yoff = plotHeight + 1; + } - if (ctx.lineWidth == 1) { - if (axis.direction == "x") { + if (ctx.lineWidth === 1) { + if (axis.direction === "x") { y = Math.floor(y) + 0.5; } else { x = Math.floor(x) + 0.5; @@ -2083,33 +2279,36 @@ Licensed under the MIT license. xoff = yoff = 0; - if (isNaN(v) || v < axis.min || v > axis.max + if (isNaN(v) || v < axis.min || v > axis.max || ( // skip those lying on the axes if we got a border - || (t == "full" - && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) - && (v == axis.min || v == axis.max))) + t === "full" && ((typeof bw === "object" && bw[axis.position] > 0) || bw > 0) && + (v === axis.min || v === axis.max) + )) { continue; + } - if (axis.direction == "x") { + if (axis.direction === "x") { x = axis.p2c(v); - yoff = t == "full" ? -plotHeight : t; + yoff = t === "full" ? -plotHeight : t; - if (axis.position == "top") + if (axis.position === "top") { yoff = -yoff; - } - else { + } + } else { y = axis.p2c(v); - xoff = t == "full" ? -plotWidth : t; + xoff = t === "full" ? -plotWidth : t; - if (axis.position == "left") + if (axis.position === "left") { xoff = -xoff; + } } - if (ctx.lineWidth == 1) { - if (axis.direction == "x") + if (ctx.lineWidth === 1) { + if (axis.direction === "x") { x = Math.floor(x) + 0.5; - else + } else { y = Math.floor(y) + 0.5; + } } ctx.moveTo(x, y); @@ -2125,7 +2324,7 @@ Licensed under the MIT license. // If either borderWidth or borderColor is an object, then draw the border // line by line instead of as one rectangle bc = options.grid.borderColor; - if(typeof bw == "object" || typeof bc == "object") { + if(typeof bw === "object" || typeof bc === "object") { if (typeof bw !== "object") { bw = {top: bw, right: bw, bottom: bw, left: bw}; } @@ -2168,8 +2367,7 @@ Licensed under the MIT license. ctx.lineTo(0- bw.left/2, 0); ctx.stroke(); } - } - else { + } else { ctx.lineWidth = bw; ctx.strokeStyle = options.grid.borderColor; ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); @@ -2182,31 +2380,48 @@ Licensed under the MIT license. function drawAxisLabels() { $.each(allAxes(), function (_, axis) { + if (!axis.show || axis.ticks.length === 0) { + return; + } + var box = axis.box, - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = axis.options.font || "flot-tick-label tickLabel", + axisOptions = axis.options, + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + axis.direction + "Axis " + axis.direction + axis.n + "Axis", + labelFont = axisOptions.labelFont || "flot-axis-label axisLabels " + axis.direction + axis.n + "axisLabel", + tickFont = axisOptions.tickFont || "flot-tick-label tickLabel", tick, x, y, halign, valign; - // Remove text before checking for axis.show and ticks.length; - // otherwise plugins, like flot-tickrotor, that draw their own - // tick labels will end up with both theirs and the defaults. - surface.removeText(layer); - if (!axis.show || axis.ticks.length == 0) - return; + if (axisOptions.label) { + if (axis.direction === "x") { + if (axisOptions.position === "top") { + surface.addText(layer, box.left + box.width / 2, box.top, axisOptions.label, labelFont, 0, null, "center", "top"); + } else { + surface.addText(layer, box.left + box.width / 2, box.top + box.height, axisOptions.label, labelFont, 0, null, "center", "bottom"); + } + } else { + if (axisOptions.position === "right") { + surface.addText(layer, box.left + box.width, box.top + box.height / 2, axisOptions.label, labelFont, 90, null, "right", "middle"); + } else { + surface.addText(layer, box.left, box.top + box.height / 2, axisOptions.label, labelFont, -90, null, "left", "middle"); + } + } + } + + // Add labels for the ticks on this axis for (var i = 0; i < axis.ticks.length; ++i) { tick = axis.ticks[i]; - if (!tick.label || tick.v < axis.min || tick.v > axis.max) + if (!tick.label || tick.v < axis.min || tick.v > axis.max) { continue; + } - if (axis.direction == "x") { + if (axis.direction === "x") { halign = "center"; x = plotOffset.left + axis.p2c(tick.v); - if (axis.position == "bottom") { + if (axis.position === "bottom") { y = box.top + box.padding; } else { y = box.top + box.height - box.padding; @@ -2215,7 +2430,7 @@ Licensed under the MIT license. } else { valign = "middle"; y = plotOffset.top + axis.p2c(tick.v); - if (axis.position == "left") { + if (axis.position === "left") { x = box.left + box.width - box.padding; halign = "right"; } else { @@ -2223,18 +2438,21 @@ Licensed under the MIT license. } } - surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); + surface.addText(layer, x, y, tick.label, tickFont, null, null, halign, valign); } }); } function drawSeries(series) { - if (series.lines.show) + if (series.lines.show) { drawSeriesLines(series); - if (series.bars.show) + } + if (series.bars.show) { drawSeriesBars(series); - if (series.points.show) + } + if (series.points.show) { drawSeriesPoints(series); + } } function drawSeriesLines(series) { @@ -2248,68 +2466,74 @@ Licensed under the MIT license. var x1 = points[i - ps], y1 = points[i - ps + 1], x2 = points[i], y2 = points[i + 1]; - if (x1 == null || x2 == null) + if (x1 == null || x2 == null) { continue; + } // clip with ymin if (y1 <= y2 && y1 < axisy.min) { - if (y2 < axisy.min) + if (y2 < axisy.min) { continue; // line segment is outside + } // compute new intersection point x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min) { - if (y1 < axisy.min) + } else if (y2 <= y1 && y2 < axisy.min) { + if (y1 < axisy.min) { continue; + } x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; y2 = axisy.min; } // clip with ymax if (y1 >= y2 && y1 > axisy.max) { - if (y2 > axisy.max) + if (y2 > axisy.max) { continue; + } x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max) { - if (y1 > axisy.max) + } else if (y2 >= y1 && y2 > axisy.max) { + if (y1 > axisy.max) { continue; + } x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; y2 = axisy.max; } // clip with xmin if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) + if (x2 < axisx.min) { continue; + } y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) + } else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) { continue; + } y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; x2 = axisx.min; } // clip with xmax if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) + if (x2 > axisx.max) { continue; + } y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) + } else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) { continue; + } y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; x2 = axisx.max; } - if (x1 != prevx || y1 != prevy) + if (x1 !== prevx || y1 !== prevy) { ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); + } prevx = x2; prevy = y2; @@ -2322,15 +2546,16 @@ Licensed under the MIT license. var points = datapoints.points, ps = datapoints.pointsize, bottom = Math.min(Math.max(0, axisy.min), axisy.max), - i = 0, top, areaOpen = false, + i = 0, areaOpen = false, ypos = 1, segmentStart = 0, segmentEnd = 0; // we process each segment in two turns, first forward // direction to sketch out top, then once we hit the // end we go backwards to sketch the bottom while (true) { - if (ps > 0 && i > points.length + ps) + if (ps > 0 && i > points.length + ps) { break; + } i += ps; // ps is negative if going backwards @@ -2347,7 +2572,7 @@ Licensed under the MIT license. continue; } - if (ps < 0 && i == segmentStart + ps) { + if (ps < 0 && i === segmentStart + ps) { // done with the reverse sweep ctx.fill(); areaOpen = false; @@ -2358,35 +2583,38 @@ Licensed under the MIT license. } } - if (x1 == null || x2 == null) + if (x1 == null || x2 == null) { continue; + } // clip x values // clip with xmin if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) + if (x2 < axisx.min) { continue; + } y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) + } else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) { continue; + } y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; x2 = axisx.min; } // clip with xmax if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) + if (x2 > axisx.max) { continue; + } y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) + } else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) { continue; + } y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; x2 = axisx.max; } @@ -2403,8 +2631,7 @@ Licensed under the MIT license. ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); continue; - } - else if (y1 <= axisy.min && y2 <= axisy.min) { + } else if (y1 <= axisy.min && y2 <= axisy.min) { ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); continue; @@ -2423,8 +2650,7 @@ Licensed under the MIT license. if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { + } else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; y2 = axisy.min; } @@ -2433,15 +2659,14 @@ Licensed under the MIT license. if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { + } else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; y2 = axisy.max; } // if the x value was changed we got a rectangle // to fill - if (x1 != x1old) { + if (x1 !== x1old) { ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); // it goes to (x1, y1), but we fill that below } @@ -2453,7 +2678,7 @@ Licensed under the MIT license. ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); // fill the other rectangle if it's there - if (x2 != x2old) { + if (x2 !== x2old) { ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); } @@ -2486,8 +2711,9 @@ Licensed under the MIT license. plotLineArea(series.datapoints, series.xaxis, series.yaxis); } - if (lw > 0) + if (lw > 0) { plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); + } ctx.restore(); } @@ -2497,16 +2723,18 @@ Licensed under the MIT license. for (var i = 0; i < points.length; i += ps) { var x = points[i], y = points[i + 1]; - if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) { continue; + } ctx.beginPath(); x = axisx.p2c(x); y = axisy.p2c(y) + offset; - if (symbol == "circle") + if (symbol === "circle") { ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); - else + } else { symbol(ctx, x, y, radius, shadow); + } ctx.closePath(); if (fillStyle) { @@ -2530,8 +2758,9 @@ Licensed under the MIT license. // Doing the conditional here allows the shadow setting to still be // optional even with a lineWidth of 0. - if( lw == 0 ) + if( lw === 0 ) { lw = 0.0001; + } if (lw > 0 && sw > 0) { // draw shadow in two steps @@ -2547,14 +2776,14 @@ Licensed under the MIT license. } ctx.lineWidth = lw; - ctx.strokeStyle = series.color; + ctx.strokeStyle = series.points.strokeColor || series.color; plotPoints(series.datapoints, radius, getFillStyle(series.points, series.color), 0, false, series.xaxis, series.yaxis, symbol); ctx.restore(); } - function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { + function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { var left, right, bottom, top, drawLeft, drawRight, drawTop, drawBottom, tmp; @@ -2578,8 +2807,7 @@ Licensed under the MIT license. drawLeft = true; drawRight = false; } - } - else { + } else { drawLeft = drawRight = drawTop = true; drawBottom = false; left = x + barLeft; @@ -2599,8 +2827,9 @@ Licensed under the MIT license. // clip if (right < axisx.min || left > axisx.max || - top < axisy.min || bottom > axisy.max) + top < axisy.min || bottom > axisy.max) { return; + } if (left < axisx.min) { left = axisx.min; @@ -2629,8 +2858,13 @@ Licensed under the MIT license. // fill the bar if (fillStyleCallback) { + c.beginPath(); + c.moveTo(left, bottom); + c.lineTo(left, top); + c.lineTo(right, top); + c.lineTo(right, bottom); c.fillStyle = fillStyleCallback(bottom, top); - c.fillRect(left, top, right - left, bottom - top) + c.fill(); } // draw outline @@ -2638,35 +2872,40 @@ Licensed under the MIT license. c.beginPath(); // FIXME: inline moveTo is buggy with excanvas - c.moveTo(left, bottom); - if (drawLeft) - c.lineTo(left, top); - else - c.moveTo(left, top); - if (drawTop) - c.lineTo(right, top); - else - c.moveTo(right, top); - if (drawRight) - c.lineTo(right, bottom); - else - c.moveTo(right, bottom); - if (drawBottom) - c.lineTo(left, bottom); - else - c.moveTo(left, bottom); + c.moveTo(left, bottom + offset); + if (drawLeft) { + c.lineTo(left, top + offset); + } else { + c.moveTo(left, top + offset); + } + if (drawTop) { + c.lineTo(right, top + offset); + } else { + c.moveTo(right, top + offset); + } + if (drawRight) { + c.lineTo(right, bottom + offset); + } else { + c.moveTo(right, bottom + offset); + } + if (drawBottom) { + c.lineTo(left, bottom + offset); + } else { + c.moveTo(left, bottom + offset); + } c.stroke(); } } function drawSeriesBars(series) { - function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { + function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) { var points = datapoints.points, ps = datapoints.pointsize; for (var i = 0; i < points.length; i += ps) { - if (points[i] == null) + if (points[i] == null) { continue; - drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); + } + drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); } } @@ -2680,53 +2919,53 @@ Licensed under the MIT license. var barLeft; switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; + case "left": + barLeft = 0; + break; + case "right": + barLeft = -series.bars.barWidth; + break; + case "center": + barLeft = -series.bars.barWidth / 2; + break; + default: + throw new Error("Invalid bar alignment: " + series.bars.align); } var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; - plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); + plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis); ctx.restore(); } function getFillStyle(filloptions, seriesColor, bottom, top) { var fill = filloptions.fill; - if (!fill) + if (!fill) { return null; + } - if (filloptions.fillColor) + if (filloptions.fillColor) { return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); + } var c = $.color.parse(seriesColor); - c.a = typeof fill == "number" ? fill : 0.4; + c.a = isNumeric(fill) ? fill : 0.4; c.normalize(); return c.toString(); } function insertLegend() { - if (options.legend.container != null) { - $(options.legend.container).html(""); - } else { - placeholder.find(".legend").remove(); - } + placeholder.find(".legend").remove(); if (!options.legend.show) { return; } - var fragments = [], entries = [], rowStarted = false, - lf = options.legend.labelFormatter, s, label; + var entries = [], lf = options.legend.labelFormatter, s, label, i; // Build a list of legend entries, with each having a label and a color - for (var i = 0; i < series.length; ++i) { + for (i = 0; i < series.length; ++i) { s = series[i]; if (s.label) { label = lf ? lf(s.label, s) : s.label; @@ -2739,18 +2978,24 @@ Licensed under the MIT license. } } + // No entries implies no legend + + if (entries.length === 0) { + return; + } + // Sort the legend using either the default or a custom comparator if (options.legend.sorted) { if ($.isFunction(options.legend.sorted)) { entries.sort(options.legend.sorted); - } else if (options.legend.sorted == "reverse") { - entries.reverse(); + } else if (options.legend.sorted === "reverse") { + entries.reverse(); } else { - var ascending = options.legend.sorted != "descending"; + var ascending = options.legend.sorted !== "descending"; entries.sort(function(a, b) { - return a.label == b.label ? 0 : ( - (a.label < b.label) != ascending ? 1 : -1 // Logical XOR + return a.label === b.label ? 0 : ( + (a.label < b.label) !== ascending ? 1 : -1 // Logical XOR ); }); } @@ -2758,63 +3003,86 @@ Licensed under the MIT license. // Generate markup for the list of entries, in their final order - for (var i = 0; i < entries.length; ++i) { + var table = $("
").css({ + "font-size": "smaller", + "color": options.grid.color + }), rowBuffer = null; + + for (i = 0; i < entries.length; ++i) { var entry = entries[i]; - if (i % options.legend.noColumns == 0) { - if (rowStarted) - fragments.push(''); - fragments.push(''); - rowStarted = true; + if (i % options.legend.noColumns === 0) { + if (rowBuffer !== null) { + table.append(rowBuffer); + } + rowBuffer = $(""); } - fragments.push( - '
' + - '' + entry.label + '' + var colorbox = $("
").css({ + "width": "4px", + "height": 0, + "border": "5px solid " + entry.color, + "overflow": "hidden" + }), + + borderbox = $("
").css({ + "border": "1px solid " + options.legend.labelBoxBorderColor, + "padding": "1px" + }); + + rowBuffer.append( + $("").addClass("legendColorBox").append(borderbox.append(colorbox)), + $("").addClass("legendLabel").html(entry.label) ); } - if (rowStarted) - fragments.push(''); + table.append(rowBuffer); - if (fragments.length == 0) - return; - - var table = '' + fragments.join("") + '
'; - if (options.legend.container != null) + if (options.legend.container != null) { $(options.legend.container).html(table); - else { - var pos = "", + } else { + var pos = {"position": "absolute"}, p = options.legend.position, m = options.legend.margin; - if (m[0] == null) + if (m[0] == null) { m = [m, m]; - if (p.charAt(0) == "n") - pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; - else if (p.charAt(0) == "s") - pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; - if (p.charAt(1) == "e") - pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; - else if (p.charAt(1) == "w") - pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; - var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); - if (options.legend.backgroundOpacity != 0.0) { + } + if (p.charAt(0) === "n") { + pos.top = (m[1] + plotOffset.top) + "px"; + } else if (p.charAt(0) === "s") { + pos.bottom = (m[1] + plotOffset.bottom) + "px"; + } + if (p.charAt(1) === "e") { + pos.right = (m[0] + plotOffset.right) + "px"; + } else if (p.charAt(1) === "w") { + pos.left = (m[0] + plotOffset.left) + "px"; + } + var legend = $("
").addClass("legend").append(table.css(pos)).appendTo(placeholder); + if (options.legend.backgroundOpacity !== 0.0) { // put in the transparent background // separately to avoid blended labels and // label boxes var c = options.legend.backgroundColor; if (c == null) { c = options.grid.backgroundColor; - if (c && typeof c == "string") + if (c && typeof c === "string") { c = $.color.parse(c); - else - c = $.color.extract(legend, 'background-color'); + } else { + c = $.color.extract(legend, "background-color"); + } c.a = 1; c = c.toString(); } var div = legend.children(); - $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); + + // Position also applies to this + $("
").css(pos).css({ + "width": div.width() + "px", + "height": div.height() + "px", + "background-color": c, + "opacity": options.legend.backgroundOpacity + }).prependTo(legend); } } } @@ -2829,11 +3097,12 @@ Licensed under the MIT license. function findNearbyItem(mouseX, mouseY, seriesFilter) { var maxDistance = options.grid.mouseActiveRadius, smallestDistance = maxDistance * maxDistance + 1, - item = null, foundPoint = false, i, j, ps; + item = null, i, j, ps; for (i = series.length - 1; i >= 0; --i) { - if (!seriesFilter(series[i])) + if (!seriesFilter(series[i])) { continue; + } var s = series[i], axisx = s.xaxis, @@ -2842,27 +3111,35 @@ Licensed under the MIT license. mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster my = axisy.c2p(mouseY), maxx = maxDistance / axisx.scale, - maxy = maxDistance / axisy.scale; + maxy = maxDistance / axisy.scale, + x, y; ps = s.datapoints.pointsize; // with inverse transforms, we can't use the maxx/maxy // optimization, sadly - if (axisx.options.inverseTransform) + if (axisx.options.inverseTransform) { maxx = Number.MAX_VALUE; - if (axisy.options.inverseTransform) + } + if (axisy.options.inverseTransform) { maxy = Number.MAX_VALUE; + } if (s.lines.show || s.points.show) { for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1]; - if (x == null) + + x = points[j]; + y = points[j + 1]; + + if (x == null) { continue; + } // For points and lines, the cursor must be within a // certain distance to the data point if (x - mx > maxx || x - mx < -maxx || - y - my > maxy || y - my < -maxy) + y - my > maxy || y - my < -maxy) { continue; + } // We have to calculate distances in pixels, not in // data units, because the scales of the axes may be different @@ -2880,34 +3157,25 @@ Licensed under the MIT license. } if (s.bars.show && !item) { // no other point can be nearby - - var barLeft, barRight; - - switch (s.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -s.bars.barWidth; - break; - default: - barLeft = -s.bars.barWidth / 2; - } - - barRight = barLeft + s.bars.barWidth; + var barLeft = s.bars.align === "left" ? 0 : -s.bars.barWidth/2, + barRight = barLeft + s.bars.barWidth; for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1], b = points[j + 2]; - if (x == null) + x = points[j]; + y = points[j + 1]; + var b = points[j + 2]; + if (x == null) { continue; + } // for a bar graph, the cursor must be inside the bar if (series[i].bars.horizontal ? (mx <= Math.max(b, x) && mx >= Math.min(b, x) && my >= y + barLeft && my <= y + barRight) : (mx >= x + barLeft && mx <= x + barRight && - my >= Math.min(b, y) && my <= Math.max(b, y))) - item = [i, j / ps]; + my >= Math.min(b, y) && my <= Math.max(b, y))) { + item = [i, j / ps]; + } } } } @@ -2927,20 +3195,22 @@ Licensed under the MIT license. } function onMouseMove(e) { - if (options.grid.hoverable) + if (options.grid.hoverable) { triggerClickHoverEvent("plothover", e, - function (s) { return s["hoverable"] != false; }); + function (s) { return s.hoverable !== false; }); + } } function onMouseLeave(e) { - if (options.grid.hoverable) + if (options.grid.hoverable) { triggerClickHoverEvent("plothover", e, - function (s) { return false; }); + function () { return false; }); + } } function onClick(e) { triggerClickHoverEvent("plotclick", e, - function (s) { return s["clickable"] != false; }); + function (s) { return s.clickable !== false; }); } // trigger click or hover event (they send the same parameters @@ -2966,15 +3236,18 @@ Licensed under the MIT license. // clear auto-highlights for (var i = 0; i < highlights.length; ++i) { var h = highlights[i]; - if (h.auto == eventname && - !(item && h.series == item.series && - h.point[0] == item.datapoint[0] && - h.point[1] == item.datapoint[1])) + if (h.auto === eventname && !( + item && h.series === item.series && + h.point[0] === item.datapoint[0] && + h.point[1] === item.datapoint[1] + )) { unhighlight(h.series, h.point); + } } - if (item) + if (item) { highlight(item.series, item.datapoint, eventname); + } } placeholder.trigger(eventname, [ pos, item ]); @@ -2982,13 +3255,14 @@ Licensed under the MIT license. function triggerRedrawOverlay() { var t = options.interaction.redrawOverlayInterval; - if (t == -1) { // skip event queue + if (t === -1) { // skip event queue drawOverlay(); return; } - if (!redrawTimeout) + if (!redrawTimeout) { redrawTimeout = setTimeout(drawOverlay, t); + } } function drawOverlay() { @@ -3003,10 +3277,11 @@ Licensed under the MIT license. for (i = 0; i < highlights.length; ++i) { hi = highlights[i]; - if (hi.series.bars.show) + if (hi.series.bars.show) { drawBarHighlight(hi.series, hi.point); - else + } else { drawPointHighlight(hi.series, hi.point); + } } octx.restore(); @@ -3014,22 +3289,22 @@ Licensed under the MIT license. } function highlight(s, point, auto) { - if (typeof s == "number") + if (isNumeric(s)) { s = series[s]; + } - if (typeof point == "number") { + if (isNumeric(point)) { var ps = s.datapoints.pointsize; point = s.datapoints.points.slice(ps * point, ps * (point + 1)); } var i = indexOfHighlight(s, point); - if (i == -1) { + if (i === -1) { highlights.push({ series: s, point: point, auto: auto }); - triggerRedrawOverlay(); - } - else if (!auto) + } else if (!auto) { highlights[i].auto = false; + } } function unhighlight(s, point) { @@ -3039,18 +3314,18 @@ Licensed under the MIT license. return; } - if (typeof s == "number") + if (isNumeric(s)) { s = series[s]; + } - if (typeof point == "number") { + if (isNumeric(point)) { var ps = s.datapoints.pointsize; point = s.datapoints.points.slice(ps * point, ps * (point + 1)); } var i = indexOfHighlight(s, point); - if (i != -1) { + if (i !== -1) { highlights.splice(i, 1); - triggerRedrawOverlay(); } } @@ -3058,9 +3333,9 @@ Licensed under the MIT license. function indexOfHighlight(s, p) { for (var i = 0; i < highlights.length; ++i) { var h = highlights[i]; - if (h.series == s && h.point[0] == p[0] - && h.point[1] == p[1]) + if (h.series === s && h.point[0] === p[0] && h.point[1] === p[1]) { return i; + } } return -1; } @@ -3068,54 +3343,52 @@ Licensed under the MIT license. function drawPointHighlight(series, point) { var x = point[0], y = point[1], axisx = series.xaxis, axisy = series.yaxis, - highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); + highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale("a", 0.5).toString(); - if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) { return; + } - var pointRadius = series.points.radius + series.points.lineWidth / 2; + var pointRadius; + var radius; + if (series.points.show) { + pointRadius = series.points.radius + series.points.lineWidth / 2; + radius = 1.5 * pointRadius; + } else { + pointRadius = series.points.radius; + radius = 0.5 * pointRadius; + } octx.lineWidth = pointRadius; octx.strokeStyle = highlightColor; - var radius = 1.5 * pointRadius; x = axisx.p2c(x); y = axisy.p2c(y); octx.beginPath(); - if (series.points.symbol == "circle") + if (series.points.symbol === "circle") { octx.arc(x, y, radius, 0, 2 * Math.PI, false); - else + } else { series.points.symbol(octx, x, y, radius, false); + } octx.closePath(); octx.stroke(); } function drawBarHighlight(series, point) { - var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), + var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale("a", 0.5).toString(), fillStyle = highlightColor, - barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } + barLeft = series.bars.align === "left" ? 0 : -series.bars.barWidth/2; octx.lineWidth = series.bars.lineWidth; octx.strokeStyle = highlightColor; drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, - function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); + 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); } function getColorOrGradient(spec, bottom, top, defaultColor) { - if (typeof spec == "string") + if (typeof spec === "string") { return spec; - else { + } else { // assume this is a gradient spec; IE currently only // supports a simple vertical gradient properly, so that's // what we support too @@ -3123,12 +3396,14 @@ Licensed under the MIT license. for (var i = 0, l = spec.colors.length; i < l; ++i) { var c = spec.colors[i]; - if (typeof c != "string") { + if (typeof c !== "string") { var co = $.color.parse(defaultColor); - if (c.brightness != null) - co = co.scale('rgb', c.brightness); - if (c.opacity != null) + if (c.brightness != null) { + co = co.scale("rgb", c.brightness); + } + if (c.opacity != null) { co.a *= c.opacity; + } c = co.toString(); } gradient.addColorStop(i / (l - 1), c); @@ -3148,7 +3423,7 @@ Licensed under the MIT license. return plot; }; - $.plot.version = "0.8.3"; + $.plot.version = "0.9.0-alpha"; $.plot.plugins = []; @@ -3165,4 +3440,4 @@ Licensed under the MIT license. return base * Math.floor(n / base); } -})(jQuery); +})(jQuery); \ No newline at end of file