From 008ff9df93b64f3f48ca429dc0f88030804e3f20 Mon Sep 17 00:00:00 2001 From: Kristjan Komlosi Date: Fri, 1 Nov 2019 20:36:32 +0100 Subject: [PATCH] added flot as the new plot library --- frontend/lib/flot/jquery.canvaswrapper.js | 550 + frontend/lib/flot/jquery.colorhelpers.js | 199 + frontend/lib/flot/jquery.flot.axislabels.js | 212 + frontend/lib/flot/jquery.flot.browser.js | 98 + frontend/lib/flot/jquery.flot.categories.js | 202 + .../lib/flot/jquery.flot.composeImages.js | 330 + frontend/lib/flot/jquery.flot.crosshair.js | 202 + frontend/lib/flot/jquery.flot.drawSeries.js | 663 ++ frontend/lib/flot/jquery.flot.errorbars.js | 375 + frontend/lib/flot/jquery.flot.fillbetween.js | 254 + frontend/lib/flot/jquery.flot.flatdata.js | 47 + frontend/lib/flot/jquery.flot.hover.js | 350 + frontend/lib/flot/jquery.flot.image.js | 249 + frontend/lib/flot/jquery.flot.js | 2787 +++++ frontend/lib/flot/jquery.flot.legend.js | 437 + frontend/lib/flot/jquery.flot.logaxis.js | 298 + frontend/lib/flot/jquery.flot.navigate.js | 798 ++ frontend/lib/flot/jquery.flot.pie.js | 786 ++ frontend/lib/flot/jquery.flot.resize.js | 60 + frontend/lib/flot/jquery.flot.saturated.js | 43 + frontend/lib/flot/jquery.flot.selection.js | 517 + frontend/lib/flot/jquery.flot.stack.js | 220 + frontend/lib/flot/jquery.flot.symbol.js | 98 + frontend/lib/flot/jquery.flot.threshold.js | 143 + frontend/lib/flot/jquery.flot.time.js | 585 + frontend/lib/flot/jquery.flot.touch.js | 320 + .../lib/flot/jquery.flot.touchNavigate.js | 360 + frontend/lib/flot/jquery.flot.uiConstants.js | 10 + frontend/lib/flot/jquery.js | 9473 +++++++++++++++++ 29 files changed, 20666 insertions(+) create mode 100644 frontend/lib/flot/jquery.canvaswrapper.js create mode 100644 frontend/lib/flot/jquery.colorhelpers.js create mode 100644 frontend/lib/flot/jquery.flot.axislabels.js create mode 100644 frontend/lib/flot/jquery.flot.browser.js create mode 100644 frontend/lib/flot/jquery.flot.categories.js create mode 100644 frontend/lib/flot/jquery.flot.composeImages.js create mode 100644 frontend/lib/flot/jquery.flot.crosshair.js create mode 100644 frontend/lib/flot/jquery.flot.drawSeries.js create mode 100644 frontend/lib/flot/jquery.flot.errorbars.js create mode 100644 frontend/lib/flot/jquery.flot.fillbetween.js create mode 100644 frontend/lib/flot/jquery.flot.flatdata.js create mode 100644 frontend/lib/flot/jquery.flot.hover.js create mode 100644 frontend/lib/flot/jquery.flot.image.js create mode 100644 frontend/lib/flot/jquery.flot.js create mode 100644 frontend/lib/flot/jquery.flot.legend.js create mode 100644 frontend/lib/flot/jquery.flot.logaxis.js create mode 100644 frontend/lib/flot/jquery.flot.navigate.js create mode 100644 frontend/lib/flot/jquery.flot.pie.js create mode 100644 frontend/lib/flot/jquery.flot.resize.js create mode 100644 frontend/lib/flot/jquery.flot.saturated.js create mode 100644 frontend/lib/flot/jquery.flot.selection.js create mode 100644 frontend/lib/flot/jquery.flot.stack.js create mode 100644 frontend/lib/flot/jquery.flot.symbol.js create mode 100644 frontend/lib/flot/jquery.flot.threshold.js create mode 100644 frontend/lib/flot/jquery.flot.time.js create mode 100644 frontend/lib/flot/jquery.flot.touch.js create mode 100644 frontend/lib/flot/jquery.flot.touchNavigate.js create mode 100644 frontend/lib/flot/jquery.flot.uiConstants.js create mode 100644 frontend/lib/flot/jquery.js diff --git a/frontend/lib/flot/jquery.canvaswrapper.js b/frontend/lib/flot/jquery.canvaswrapper.js new file mode 100644 index 0000000..355fddf --- /dev/null +++ b/frontend/lib/flot/jquery.canvaswrapper.js @@ -0,0 +1,550 @@ +/** ## jquery.flot.canvaswrapper + +This plugin contains the function for creating and manipulating both the canvas +layers and svg layers. + +The Canvas object is a wrapper around an HTML5 canvas tag. +The constructor Canvas(cls, container) takes as parameters cls, +the list of classes to apply to the canvas adnd the containter, +element onto which to append the canvas. The canvas operations +don't work unless the canvas is attached to the DOM. + +### jquery.canvaswrapper.js API functions +*/ + +(function($) { + var Canvas = function(cls, container) { + var element = container.getElementsByClassName(cls)[0]; + + if (!element) { + element = document.createElement('canvas'); + element.className = cls; + element.style.direction = 'ltr'; + element.style.position = 'absolute'; + element.style.left = '0px'; + element.style.top = '0px'; + + container.appendChild(element); + + // If HTML5 Canvas isn't available, throw + + if (!element.getContext) { + throw new Error('Canvas is not available.'); + } + } + + this.element = element; + + var context = this.context = element.getContext('2d'); + this.pixelRatio = $.plot.browser.getPixelRatio(context); + + // Size the canvas to match the internal dimensions of its container + var width = $(container).width(); + var height = $(container).height(); + this.resize(width, height); + + // Collection of HTML div layers for text overlaid onto the canvas + + this.SVGContainer = null; + this.SVG = {}; + + // Cache of text fragments and metrics, so we can avoid expensively + // re-calculating them when the plot is re-rendered in a loop. + + this._textCache = {}; + } + + /** + - resize(width, height) + + Resizes the canvas to the given dimensions. + The width represents the new width of the canvas, meanwhile the height + is the new height of the canvas, both of them in pixels. + */ + + Canvas.prototype.resize = function(width, height) { + var minSize = 10; + width = width < minSize ? minSize : width; + height = height < minSize ? minSize : height; + + var element = this.element, + context = this.context, + pixelRatio = this.pixelRatio; + + // Resize the canvas, increasing its density based on the display's + // pixel ratio; basically giving it more pixels without increasing the + // size of its element, to take advantage of the fact that retina + // displays have that many more pixels in the same advertised space. + + // Resizing should reset the state (excanvas seems to be buggy though) + + if (this.width !== width) { + element.width = width * pixelRatio; + element.style.width = width + 'px'; + this.width = width; + } + + if (this.height !== height) { + element.height = height * pixelRatio; + element.style.height = height + 'px'; + this.height = height; + } + + // Save the context, so we can reset in case we get replotted. The + // restore ensure that we're really back at the initial state, and + // should be safe even if we haven't saved the initial state yet. + + context.restore(); + context.save(); + + // Scale the coordinate space to match the display density; so even though we + // may have twice as many pixels, we still want lines and other drawing to + // appear at the same size; the extra pixels will just make them crisper. + + context.scale(pixelRatio, pixelRatio); + }; + + /** + - clear() + + Clears the entire canvas area, not including any overlaid HTML text + */ + Canvas.prototype.clear = function() { + this.context.clearRect(0, 0, this.width, this.height); + }; + + /** + - render() + + Finishes rendering the canvas, including managing the text overlay. + */ + Canvas.prototype.render = function() { + var cache = this._textCache; + + // For each text layer, add elements marked as active that haven't + // already been rendered, and remove those that are no longer active. + + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { + var layer = this.getSVGLayer(layerKey), + layerCache = cache[layerKey]; + + var display = layer.style.display; + layer.style.display = 'none'; + + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + var val = styleCache[key], + positions = val.positions; + + for (var i = 0, position; positions[i]; i++) { + position = positions[i]; + if (position.active) { + if (!position.rendered) { + layer.appendChild(position.element); + position.rendered = true; + } + } else { + positions.splice(i--, 1); + if (position.rendered) { + while (position.element.firstChild) { + position.element.removeChild(position.element.firstChild); + } + position.element.parentNode.removeChild(position.element); + } + } + } + + if (positions.length === 0) { + if (val.measured) { + val.measured = false; + } else { + delete styleCache[key]; + } + } + } + } + } + } + + layer.style.display = display; + } + } + }; + + /** + - getSVGLayer(classes) + + Creates (if necessary) and returns the SVG overlay container. + The classes string represents the string of space-separated CSS classes + used to uniquely identify the text layer. It return the svg-layer div. + */ + Canvas.prototype.getSVGLayer = function(classes) { + var layer = this.SVG[classes]; + + // Create the SVG layer if it doesn't exist + + if (!layer) { + // Create the svg layer container, if it doesn't exist + + var svgElement; + + if (!this.SVGContainer) { + this.SVGContainer = document.createElement('div'); + this.SVGContainer.className = 'flot-svg'; + this.SVGContainer.style.position = 'absolute'; + this.SVGContainer.style.top = '0px'; + this.SVGContainer.style.left = '0px'; + this.SVGContainer.style.height = '100%'; + this.SVGContainer.style.width = '100%'; + this.SVGContainer.style.pointerEvents = 'none'; + this.element.parentNode.appendChild(this.SVGContainer); + + svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svgElement.style.width = '100%'; + svgElement.style.height = '100%'; + + this.SVGContainer.appendChild(svgElement); + } else { + svgElement = this.SVGContainer.firstChild; + } + + layer = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + layer.setAttribute('class', classes); + layer.style.position = 'absolute'; + layer.style.top = '0px'; + layer.style.left = '0px'; + layer.style.bottom = '0px'; + layer.style.right = '0px'; + svgElement.appendChild(layer); + this.SVG[classes] = layer; + } + + return layer; + }; + + /** + - getTextInfo(layer, text, font, angle, width) + + Creates (if necessary) and returns a text info object. + The object looks like this: + ```js + { + width //Width of the text's wrapper div. + height //Height of the text's wrapper div. + element //The HTML div containing the text. + positions //Array of positions at which this text is drawn. + } + ``` + The positions array contains objects that look like this: + ```js + { + active //Flag indicating whether the text should be visible. + rendered //Flag indicating whether the text is currently visible. + element //The HTML div containing the text. + text //The actual text and is identical with element[0].textContent. + x //X coordinate at which to draw the text. + y //Y coordinate at which to draw the text. + } + ``` + Each position after the first receives a clone of the original element. + The idea is that that the width, height, and general 'identity' of the + text is constant no matter where it is placed; the placements are a + secondary property. + + Canvas maintains a cache of recently-used text info objects; getTextInfo + either returns the cached element or creates a new entry. + + The layer parameter is string of space-separated CSS classes uniquely + identifying the layer containing this text. + Text is the text string to retrieve info for. + Font is either a string of space-separated CSS classes or a font-spec object, + defining the text's font and style. + Angle is the angle at which to rotate the text, in degrees. Angle is currently unused, + it will be implemented in the future. + The last parameter is the Maximum width of the text before it wraps. + The method returns a text info object. + */ + Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { + var textStyle, layerCache, styleCache, info; + + // Cast the value to a string, in case we were given a number or such + + text = '' + text; + + // If the font is a font-spec object, generate a CSS font definition + + if (typeof font === 'object') { + textStyle = font.style + ' ' + font.variant + ' ' + font.weight + ' ' + font.size + 'px/' + font.lineHeight + 'px ' + font.family; + } else { + textStyle = font; + } + + // Retrieve (or create) the cache for the text's layer and styles + + layerCache = this._textCache[layer]; + + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; + } + + styleCache = layerCache[textStyle]; + + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } + + var key = generateKey(text); + info = styleCache[key]; + + // If we can't find a matching element in our cache, create a new one + + if (!info) { + var element = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + if (text.indexOf('
') !== -1) { + addTspanElements(text, element, -9999); + } else { + var textNode = document.createTextNode(text); + element.appendChild(textNode); + } + + element.style.position = 'absolute'; + element.style.maxWidth = width; + element.setAttributeNS(null, 'x', -9999); + element.setAttributeNS(null, 'y', -9999); + + if (typeof font === 'object') { + element.style.font = textStyle; + element.style.fill = font.fill; + } else if (typeof font === 'string') { + element.setAttribute('class', font); + } + + this.getSVGLayer(layer).appendChild(element); + var elementRect = element.getBBox(); + + info = styleCache[key] = { + width: elementRect.width, + height: elementRect.height, + measured: true, + element: element, + positions: [] + }; + + //remove elements from dom + while (element.firstChild) { + element.removeChild(element.firstChild); + } + element.parentNode.removeChild(element); + } + + info.measured = true; + return info; + }; + + function updateTransforms (element, transforms) { + element.transform.baseVal.clear(); + if (transforms) { + transforms.forEach(function(t) { + element.transform.baseVal.appendItem(t); + }); + } + } + + /** + - addText (layer, x, y, text, font, angle, width, halign, valign, transforms) + + Adds a text string to the canvas text overlay. + The text isn't drawn immediately; it is marked as rendering, which will + result in its addition to the canvas on the next render pass. + + The layer is string of space-separated CSS classes uniquely + identifying the layer containing this text. + X and Y represents the X and Y coordinate at which to draw the text. + and text is the string to draw + */ + Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign, transforms) { + var info = this.getTextInfo(layer, text, font, angle, width), + positions = info.positions; + + // Tweak the div's position to match the text's alignment + + if (halign === 'center') { + x -= info.width / 2; + } else if (halign === 'right') { + x -= info.width; + } + + if (valign === 'middle') { + y -= info.height / 2; + } else if (valign === 'bottom') { + y -= info.height; + } + + y += 0.75 * info.height; + + + // Determine whether this text already exists at this position. + // If so, mark it for inclusion in the next render pass. + + for (var i = 0, position; positions[i]; i++) { + position = positions[i]; + if (position.x === x && position.y === y && position.text === text) { + position.active = true; + // update the transforms + updateTransforms(position.element, transforms); + + return; + } else if (position.active === false) { + position.active = true; + position.text = text; + if (text.indexOf('
') !== -1) { + y -= 0.25 * info.height; + addTspanElements(text, position.element, x); + } else { + position.element.textContent = text; + } + position.element.setAttributeNS(null, 'x', x); + position.element.setAttributeNS(null, 'y', y); + position.x = x; + position.y = y; + // update the transforms + updateTransforms(position.element, transforms); + + return; + } + } + + // If the text doesn't exist at this position, create a new entry + + // For the very first position we'll re-use the original element, + // while for subsequent ones we'll clone it. + + position = { + active: true, + rendered: false, + element: positions.length ? info.element.cloneNode() : info.element, + text: text, + x: x, + y: y + }; + + positions.push(position); + + if (text.indexOf('
') !== -1) { + y -= 0.25 * info.height; + addTspanElements(text, position.element, x); + } else { + position.element.textContent = text; + } + + // Move the element to its final position within the container + position.element.setAttributeNS(null, 'x', x); + position.element.setAttributeNS(null, 'y', y); + position.element.style.textAlign = halign; + // update the transforms + updateTransforms(position.element, transforms); + }; + + var addTspanElements = function(text, element, x) { + var lines = text.split('
'), + tspan, i, offset; + + for (i = 0; i < lines.length; i++) { + if (!element.childNodes[i]) { + tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); + element.appendChild(tspan); + } else { + tspan = element.childNodes[i]; + } + tspan.textContent = lines[i]; + offset = i * 1 + 'em'; + tspan.setAttributeNS(null, 'dy', offset); + tspan.setAttributeNS(null, 'x', x); + } + } + + /** + - removeText (layer, x, y, text, font, angle) + + The function removes one or more text strings from the canvas text overlay. + If no parameters are given, all text within the layer is removed. + + Note that the text is not immediately removed; it is simply marked as + inactive, which will result in its removal on the next render pass. + This avoids the performance penalty for 'clear and redraw' behavior, + where we potentially get rid of all text on a layer, but will likely + add back most or all of it later, as when redrawing axes, for example. + + The layer is a string of space-separated CSS classes uniquely + identifying the layer containing this text. The following parameter are + X and Y coordinate of the text. + Text is the string to remove, while the font is either a string of space-separated CSS + classes or a font-spec object, defining the text's font and style. + */ + Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { + var info, htmlYCoord; + if (text == null) { + var layerCache = this._textCache[layer]; + if (layerCache != null) { + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + var positions = styleCache[key].positions; + positions.forEach(function(position) { + position.active = false; + }); + } + } + } + } + } + } else { + info = this.getTextInfo(layer, text, font, angle); + positions = info.positions; + positions.forEach(function(position) { + htmlYCoord = y + 0.75 * info.height; + if (position.x === x && position.y === htmlYCoord && position.text === text) { + position.active = false; + } + }); + } + }; + + /** + - clearCache() + + Clears the cache used to speed up the text size measurements. + As an (unfortunate) side effect all text within the text Layer is removed. + Use this function before plot.setupGrid() and plot.draw() if the plot just + became visible or the styles changed. + */ + Canvas.prototype.clearCache = function() { + var cache = this._textCache; + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { + var layer = this.getSVGLayer(layerKey); + while (layer.firstChild) { + layer.removeChild(layer.firstChild); + } + } + }; + + this._textCache = {}; + }; + + function generateKey(text) { + return text.replace(/0|1|2|3|4|5|6|7|8|9/g, '0'); + } + + if (!window.Flot) { + window.Flot = {}; + } + + window.Flot.Canvas = Canvas; +})(jQuery); diff --git a/frontend/lib/flot/jquery.colorhelpers.js b/frontend/lib/flot/jquery.colorhelpers.js new file mode 100644 index 0000000..c59cf2f --- /dev/null +++ b/frontend/lib/flot/jquery.colorhelpers.js @@ -0,0 +1,199 @@ +/* Plugin for jQuery for working with colors. + * + * Version 1.1. + * + * Inspiration from jQuery color animation plugin by John Resig. + * + * Released under the MIT license by Ole Laursen, October 2009. + * + * Examples: + * + * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() + * var c = $.color.extract($("#mydiv"), 'background-color'); + * console.log(c.r, c.g, c.b, c.a); + * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" + * + * Note that .scale() and .add() return the same modified object + * instead of making a new one. + * + * V. 1.1: Fix error handling so e.g. parsing an empty string does + * produce a color rather than just crashing. + */ + +(function($) { + $.color = {}; + + // construct color object with some convenient chainable helpers + $.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 < c.length; ++i) { + o[c.charAt(i)] += d; + } + + return o.normalize(); + }; + + o.scale = function (c, f) { + for (var i = 0; i < c.length; ++i) { + o[c.charAt(i)] *= f; + } + + return o.normalize(); + }; + + o.toString = function () { + if (o.a >= 1.0) { + 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 value < min ? min : (value > max ? 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(); + } + + // extract CSS color property from element, going up in the DOM + // if it's "transparent" + $.color.extract = function (elem, css) { + var c; + + do { + c = elem.css(css).toLowerCase(); + // keep going until we find an element that has color, or + // we hit the body or root (have no parent) + if (c !== '' && c !== 'transparent') { + break; + } + + elem = elem.parent(); + } while (elem.length && !$.nodeName(elem.get(0), "body")); + + // catch Safari's way of signalling transparent + if (c === "rgba(0, 0, 0, 0)") { + c = "transparent"; + } + + return $.color.parse(c); + } + + // parse CSS color string (like "rgb(10, 32, 43)" or "#fff"), + // returns color object, if parsing failed, you get black (0, 0, + // 0) out + $.color.parse = function (str) { + var res, m = $.color.make; + + // Look for rgb(num,num,num) + res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str); + if (res) { + return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10)); + } + + // Look for rgba(num,num,num,num) + 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) + if (res) { + return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4])); + } + + // Look for rgb(num%,num%,num%) + res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)%\s*\)/.exec(str); + if (res) { + return m(parseFloat(res[1]) * 2.55, parseFloat(res[2]) * 2.55, parseFloat(res[3]) * 2.55); + } + + // Look for rgba(num%,num%,num%,num) + 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); + if (res) { + return m(parseFloat(res[1]) * 2.55, parseFloat(res[2]) * 2.55, parseFloat(res[3]) * 2.55, parseFloat(res[4])); + } + + // Look for #a0b1c2 + res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str); + if (res) { + return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)); + } + + // Look for #fff + res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str); + if (res) { + return m(parseInt(res[1] + res[1], 16), parseInt(res[2] + res[2], 16), parseInt(res[3] + res[3], 16)); + } + + // Otherwise, we're most likely dealing with a named color + var name = $.trim(str).toLowerCase(); + if (name === "transparent") { + return m(255, 255, 255, 0); + } else { + // default to black + 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); diff --git a/frontend/lib/flot/jquery.flot.axislabels.js b/frontend/lib/flot/jquery.flot.axislabels.js new file mode 100644 index 0000000..8c80828 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.axislabels.js @@ -0,0 +1,212 @@ +/* +Axis label plugin for flot + +Derived from: +Axis Labels Plugin for flot. +http://github.com/markrcote/flot-axislabels + +Original code is Copyright (c) 2010 Xuan Luo. +Original code was released under the GPLv3 license by Xuan Luo, September 2010. +Original code was rereleased under the MIT license by Xuan Luo, April 2012. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +(function($) { + "use strict"; + + var options = { + axisLabels: { + show: true + } + }; + + function AxisLabel(axisName, position, padding, placeholder, axisLabel, surface) { + this.axisName = axisName; + this.position = position; + this.padding = padding; + this.placeholder = placeholder; + this.axisLabel = axisLabel; + this.surface = surface; + this.width = 0; + this.height = 0; + this.elem = null; + } + + AxisLabel.prototype.calculateSize = function() { + var axisId = this.axisName + 'Label', + layerId = axisId + 'Layer', + className = axisId + ' axisLabels'; + + var info = this.surface.getTextInfo(layerId, this.axisLabel, className); + this.labelWidth = info.width; + this.labelHeight = info.height; + + if (this.position === 'left' || this.position === 'right') { + this.width = this.labelHeight + this.padding; + this.height = 0; + } else { + this.width = 0; + this.height = this.labelHeight + this.padding; + } + }; + + AxisLabel.prototype.transforms = function(degrees, x, y, svgLayer) { + var transforms = [], translate, rotate; + if (x !== 0 || y !== 0) { + translate = svgLayer.createSVGTransform(); + translate.setTranslate(x, y); + transforms.push(translate); + } + if (degrees !== 0) { + rotate = svgLayer.createSVGTransform(); + var centerX = Math.round(this.labelWidth / 2), + centerY = 0; + rotate.setRotate(degrees, centerX, centerY); + transforms.push(rotate); + } + + return transforms; + }; + + AxisLabel.prototype.calculateOffsets = function(box) { + var offsets = { + x: 0, + y: 0, + degrees: 0 + }; + if (this.position === 'bottom') { + offsets.x = box.left + box.width / 2 - this.labelWidth / 2; + offsets.y = box.top + box.height - this.labelHeight; + } else if (this.position === 'top') { + offsets.x = box.left + box.width / 2 - this.labelWidth / 2; + offsets.y = box.top; + } else if (this.position === 'left') { + offsets.degrees = -90; + offsets.x = box.left - this.labelWidth / 2; + offsets.y = box.height / 2 + box.top; + } else if (this.position === 'right') { + offsets.degrees = 90; + offsets.x = box.left + box.width - this.labelWidth / 2; + offsets.y = box.height / 2 + box.top; + } + offsets.x = Math.round(offsets.x); + offsets.y = Math.round(offsets.y); + + return offsets; + }; + + AxisLabel.prototype.cleanup = function() { + var axisId = this.axisName + 'Label', + layerId = axisId + 'Layer', + className = axisId + ' axisLabels'; + this.surface.removeText(layerId, 0, 0, this.axisLabel, className); + }; + + AxisLabel.prototype.draw = function(box) { + var axisId = this.axisName + 'Label', + layerId = axisId + 'Layer', + className = axisId + ' axisLabels', + offsets = this.calculateOffsets(box), + style = { + position: 'absolute', + bottom: '', + right: '', + display: 'inline-block', + 'white-space': 'nowrap' + }; + + var layer = this.surface.getSVGLayer(layerId); + var transforms = this.transforms(offsets.degrees, offsets.x, offsets.y, layer.parentNode); + + this.surface.addText(layerId, 0, 0, this.axisLabel, className, undefined, undefined, undefined, undefined, transforms); + this.surface.render(); + Object.keys(style).forEach(function(key) { + layer.style[key] = style[key]; + }); + }; + + function init(plot) { + plot.hooks.processOptions.push(function(plot, options) { + if (!options.axisLabels.show) { + return; + } + + var axisLabels = {}; + var defaultPadding = 2; // padding between axis and tick labels + + plot.hooks.axisReserveSpace.push(function(plot, axis) { + var opts = axis.options; + var axisName = axis.direction + axis.n; + + axis.labelHeight += axis.boxPosition.centerY; + axis.labelWidth += axis.boxPosition.centerX; + + if (!opts || !opts.axisLabel || !axis.show) { + return; + } + + var padding = opts.axisLabelPadding === undefined + ? defaultPadding + : opts.axisLabelPadding; + + var axisLabel = axisLabels[axisName]; + if (!axisLabel) { + axisLabel = new AxisLabel(axisName, + opts.position, padding, + plot.getPlaceholder()[0], opts.axisLabel, plot.getSurface()); + axisLabels[axisName] = axisLabel; + } + + axisLabel.calculateSize(); + + // Incrementing the sizes of the tick labels. + axis.labelHeight += axisLabel.height; + axis.labelWidth += axisLabel.width; + }); + + // TODO - use the drawAxis hook + plot.hooks.draw.push(function(plot, ctx) { + $.each(plot.getAxes(), function(flotAxisName, axis) { + var opts = axis.options; + if (!opts || !opts.axisLabel || !axis.show) { + return; + } + + var axisName = axis.direction + axis.n; + axisLabels[axisName].draw(axis.box); + }); + }); + + plot.hooks.shutdown.push(function(plot, eventHolder) { + for (var axisName in axisLabels) { + axisLabels[axisName].cleanup(); + } + }); + }); + }; + + $.plot.plugins.push({ + init: init, + options: options, + name: 'axisLabels', + version: '3.0' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.browser.js b/frontend/lib/flot/jquery.flot.browser.js new file mode 100644 index 0000000..e50a629 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.browser.js @@ -0,0 +1,98 @@ +/** ## jquery.flot.browser.js + +This plugin is used to make available some browser-related utility functions. + +### Methods +*/ + +(function ($) { + 'use strict'; + + var browser = { + /** + - getPageXY(e) + + Calculates the pageX and pageY using the screenX, screenY properties of the event + and the scrolling of the page. This is needed because the pageX and pageY + properties of the event are not correct while running tests in Edge. */ + getPageXY: function (e) { + // This code is inspired from https://stackoverflow.com/a/3464890 + var doc = document.documentElement, + pageX = e.clientX + (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0), + pageY = e.clientY + (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); + return { X: pageX, Y: pageY }; + }, + + /** + - getPixelRatio(context) + + This function returns the current pixel ratio defined by the product of desktop + zoom and page zoom. + Additional info: https://www.html5rocks.com/en/tutorials/canvas/hidpi/ + */ + getPixelRatio: function(context) { + var devicePixelRatio = window.devicePixelRatio || 1, + backingStoreRatio = + context.webkitBackingStorePixelRatio || + context.mozBackingStorePixelRatio || + context.msBackingStorePixelRatio || + context.oBackingStorePixelRatio || + context.backingStorePixelRatio || 1; + return devicePixelRatio / backingStoreRatio; + }, + + /** + - isSafari, isMobileSafari, isOpera, isFirefox, isIE, isEdge, isChrome, isBlink + + This is a collection of functions, used to check if the code is running in a + particular browser or Javascript engine. + */ + isSafari: function() { + // *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser + // Safari 3.0+ "[object HTMLElementConstructor]" + return /constructor/i.test(window.top.HTMLElement) || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window.top['safari'] || (typeof window.top.safari !== 'undefined' && window.top.safari.pushNotification)); + }, + + isMobileSafari: function() { + //isMobileSafari adapted from https://stackoverflow.com/questions/3007480/determine-if-user-navigated-from-mobile-safari + return navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/); + }, + + isOpera: function() { + // *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser + //Opera 8.0+ + return (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0; + }, + + isFirefox: function() { + // *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser + // Firefox 1.0+ + return typeof InstallTrigger !== 'undefined'; + }, + + isIE: function() { + // *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser + // Internet Explorer 6-11 + return /*@cc_on!@*/false || !!document.documentMode; + }, + + isEdge: function() { + // *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser + // Edge 20+ + return !browser.isIE() && !!window.StyleMedia; + }, + + isChrome: function() { + // *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser + // Chrome 1+ + return !!window.chrome && !!window.chrome.webstore; + }, + + isBlink: function() { + // *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser + return (browser.isChrome() || browser.isOpera()) && !!window.CSS; + } + }; + + $.plot.browser = browser; +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.categories.js b/frontend/lib/flot/jquery.flot.categories.js new file mode 100644 index 0000000..af16f78 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.categories.js @@ -0,0 +1,202 @@ +/* Flot plugin for plotting textual data or categories. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +Consider a dataset like [["February", 34], ["March", 20], ...]. This plugin +allows you to plot such a dataset directly. + +To enable it, you must specify mode: "categories" on the axis with the textual +labels, e.g. + + $.plot("#placeholder", data, { xaxis: { mode: "categories" } }); + +By default, the labels are ordered as they are met in the data series. If you +need a different ordering, you can specify "categories" on the axis options +and list the categories there: + + xaxis: { + mode: "categories", + categories: ["February", "March", "April"] + } + +If you need to customize the distances between the categories, you can specify +"categories" as an object mapping labels to values + + xaxis: { + mode: "categories", + categories: { "February": 1, "March": 3, "April": 4 } + } + +If you don't specify all categories, the remaining categories will be numbered +from the max value plus 1 (with a spacing of 1 between each). + +Internally, the plugin works by transforming the input data through an auto- +generated mapping where the first category becomes 0, the second 1, etc. +Hence, a point like ["February", 34] becomes [0, 34] internally in Flot (this +is visible in hover and click events that return numbers rather than the +category labels). The plugin also overrides the tick generator to spit out the +categories as ticks instead of the values. + +If you need to map a value back to its label, the mapping is always accessible +as "categories" on the axis object, e.g. plot.getAxes().xaxis.categories. + +*/ + +(function ($) { + var options = { + xaxis: { + categories: null + }, + yaxis: { + categories: null + } + }; + + function processRawData(plot, series, data, datapoints) { + // if categories are enabled, we need to disable + // auto-transformation to numbers so the strings are intact + // for later processing + + var xCategories = series.xaxis.options.mode === "categories", + yCategories = series.yaxis.options.mode === "categories"; + + if (!(xCategories || yCategories)) { + return; + } + + var format = datapoints.format; + + if (!format) { + // FIXME: auto-detection should really not be defined here + var s = series; + format = []; + format.push({ x: true, number: true, required: true, computeRange: true}); + format.push({ y: true, number: true, required: true, computeRange: true }); + + if (s.bars.show || (s.lines.show && s.lines.fill)) { + var autoScale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); + format.push({ y: true, number: true, required: false, defaultValue: 0, computeRange: autoScale }); + if (s.bars.horizontal) { + delete format[format.length - 1].y; + format[format.length - 1].x = true; + } + } + + datapoints.format = format; + } + + for (var m = 0; m < format.length; ++m) { + if (format[m].x && xCategories) { + format[m].number = false; + } + + if (format[m].y && yCategories) { + format[m].number = false; + format[m].computeRange = false; + } + } + } + + function getNextIndex(categories) { + var index = -1; + + for (var v in categories) { + if (categories[v] > index) { + index = categories[v]; + } + } + + return index + 1; + } + + function categoriesTickGenerator(axis) { + var res = []; + for (var label in axis.categories) { + var v = axis.categories[label]; + if (v >= axis.min && v <= axis.max) { + res.push([v, label]); + } + } + + res.sort(function (a, b) { return a[0] - b[0]; }); + + return res; + } + + function setupCategoriesForAxis(series, axis, datapoints) { + if (series[axis].options.mode !== "categories") { + return; + } + + if (!series[axis].categories) { + // parse options + var c = {}, o = series[axis].options.categories || {}; + if ($.isArray(o)) { + for (var i = 0; i < o.length; ++i) { + c[o[i]] = i; + } + } else { + for (var v in o) { + c[v] = o[v]; + } + } + + series[axis].categories = c; + } + + // fix ticks + if (!series[axis].options.ticks) { + series[axis].options.ticks = categoriesTickGenerator; + } + + transformPointsOnAxis(datapoints, axis, series[axis].categories); + } + + function transformPointsOnAxis(datapoints, axis, categories) { + // go through the points, transforming them + var points = datapoints.points, + ps = datapoints.pointsize, + format = datapoints.format, + formatColumn = axis.charAt(0), + index = getNextIndex(categories); + + for (var i = 0; i < points.length; i += ps) { + if (points[i] == null) { + continue; + } + + for (var m = 0; m < ps; ++m) { + var val = points[i + m]; + + if (val == null || !format[m][formatColumn]) { + continue; + } + + if (!(val in categories)) { + categories[val] = index; + ++index; + } + + points[i + m] = categories[val]; + } + } + } + + function processDatapoints(plot, series, datapoints) { + setupCategoriesForAxis(series, "xaxis", datapoints); + setupCategoriesForAxis(series, "yaxis", datapoints); + } + + function init(plot) { + plot.hooks.processRawData.push(processRawData); + plot.hooks.processDatapoints.push(processDatapoints); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'categories', + version: '1.0' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.composeImages.js b/frontend/lib/flot/jquery.flot.composeImages.js new file mode 100644 index 0000000..0b7ab78 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.composeImages.js @@ -0,0 +1,330 @@ +/** ## jquery.flot.composeImages.js + +This plugin is used to expose a function used to overlap several canvases and +SVGs, for the purpose of creating a snaphot out of them. + +### When composeImages is used: +When multiple canvases and SVGs have to be overlapped into a single image +and their offset on the page, must be preserved. + +### Where can be used: +In creating a downloadable snapshot of the plots, axes, cursors etc of a graph. + +### How it works: +The entry point is composeImages function. It expects an array of objects, +which should be either canvases or SVGs (or a mix). It does a prevalidation +of them, by verifying if they will be usable or not, later in the flow. +After selecting only usable sources, it passes them to getGenerateTempImg +function, which generates temporary images out of them. This function +expects that some of the passed sources (canvas or SVG) may still have +problems being converted to an image and makes sure the promises system, +used by composeImages function, moves forward. As an example, SVGs with +missing information from header or with unsupported content, may lead to +failure in generating the temporary image. Temporary images are required +mostly on extracting content from SVGs, but this is also where the x/y +offsets are extracted for each image which will be added. For SVGs in +particular, their CSS rules have to be applied. +After all temporary images are generated, they are overlapped using +getExecuteImgComposition function. This is where the destination canvas +is set to the proper dimensions. It is then output by composeImages. +This function returns a promise, which can be used to wait for the whole +composition process. It requires to be asynchronous, because this is how +temporary images load their data. +*/ + +(function($) { + "use strict"; + const GENERALFAILURECALLBACKERROR = -100; //simply a negative number + const SUCCESSFULIMAGEPREPARATION = 0; + const EMPTYARRAYOFIMAGESOURCES = -1; + const NEGATIVEIMAGESIZE = -2; + var pixelRatio = 1; + var browser = $.plot.browser; + var getPixelRatio = browser.getPixelRatio; + + function composeImages(canvasOrSvgSources, destinationCanvas) { + var validCanvasOrSvgSources = canvasOrSvgSources.filter(isValidSource); + pixelRatio = getPixelRatio(destinationCanvas.getContext('2d')); + + var allImgCompositionPromises = validCanvasOrSvgSources.map(function(validCanvasOrSvgSource) { + var tempImg = new Image(); + var currentPromise = new Promise(getGenerateTempImg(tempImg, validCanvasOrSvgSource)); + return currentPromise; + }); + + var lastPromise = Promise.all(allImgCompositionPromises).then(getExecuteImgComposition(destinationCanvas), failureCallback); + return lastPromise; + } + + function isValidSource(canvasOrSvgSource) { + var isValidFromCanvas = true; + var isValidFromContent = true; + if ((canvasOrSvgSource === null) || (canvasOrSvgSource === undefined)) { + isValidFromContent = false; + } else { + if (canvasOrSvgSource.tagName === 'CANVAS') { + if ((canvasOrSvgSource.getBoundingClientRect().right === canvasOrSvgSource.getBoundingClientRect().left) || + (canvasOrSvgSource.getBoundingClientRect().bottom === canvasOrSvgSource.getBoundingClientRect().top)) { + isValidFromCanvas = false; + } + } + } + return isValidFromContent && isValidFromCanvas && (window.getComputedStyle(canvasOrSvgSource).visibility === 'visible'); + } + + function getGenerateTempImg(tempImg, canvasOrSvgSource) { + tempImg.sourceDescription = ''; + tempImg.sourceComponent = canvasOrSvgSource; + + return function doGenerateTempImg(successCallbackFunc, failureCallbackFunc) { + tempImg.onload = function(evt) { + tempImg.successfullyLoaded = true; + successCallbackFunc(tempImg); + }; + + tempImg.onabort = function(evt) { + tempImg.successfullyLoaded = false; + console.log('Can\'t generate temp image from ' + tempImg.sourceDescription + '. It is possible that it is missing some properties or its content is not supported by this browser. Source component:', tempImg.sourceComponent); + successCallbackFunc(tempImg); //call successCallback, to allow snapshot of all working images + }; + + tempImg.onerror = function(evt) { + tempImg.successfullyLoaded = false; + console.log('Can\'t generate temp image from ' + tempImg.sourceDescription + '. It is possible that it is missing some properties or its content is not supported by this browser. Source component:', tempImg.sourceComponent); + successCallbackFunc(tempImg); //call successCallback, to allow snapshot of all working images + }; + + generateTempImageFromCanvasOrSvg(canvasOrSvgSource, tempImg); + }; + } + + function getExecuteImgComposition(destinationCanvas) { + return function executeImgComposition(tempImgs) { + var compositionResult = copyImgsToCanvas(tempImgs, destinationCanvas); + return compositionResult; + }; + } + + function copyCanvasToImg(canvas, img) { + img.src = canvas.toDataURL('image/png'); + } + + function getCSSRules(document) { + var styleSheets = document.styleSheets, + rulesList = []; + for (var i = 0; i < styleSheets.length; i++) { + // CORS requests for style sheets throw and an exception on Chrome > 64 + try { + // in Chrome, the external CSS files are empty when the page is directly loaded from disk + var rules = styleSheets[i].cssRules || []; + for (var j = 0; j < rules.length; j++) { + var rule = rules[j]; + rulesList.push(rule.cssText); + } + } catch (e) { + console.log('Failed to get some css rules'); + } + } + return rulesList; + } + + function embedCSSRulesInSVG(rules, svg) { + var text = [ + '', + '', + svg.innerHTML, + '' + ].join('\n'); + return text; + } + + function copySVGToImgMostBrowsers(svg, img) { + var rules = getCSSRules(document), + source = embedCSSRulesInSVG(rules, svg); + + source = patchSVGSource(source); + + var blob = new Blob([source], {type: "image/svg+xml;charset=utf-8"}), + domURL = self.URL || self.webkitURL || self, + url = domURL.createObjectURL(blob); + img.src = url; + } + + function copySVGToImgSafari(svg, img) { + // Use this method to convert a string buffer array to a binary string. + // Do so by breaking up large strings into smaller substrings; this is necessary to avoid the + // "maximum call stack size exceeded" exception that can happen when calling 'String.fromCharCode.apply' + // with a very long array. + function buildBinaryString (arrayBuffer) { + var binaryString = ""; + const utf8Array = new Uint8Array(arrayBuffer); + const blockSize = 16384; + for (var i = 0; i < utf8Array.length; i = i + blockSize) { + const binarySubString = String.fromCharCode.apply(null, utf8Array.subarray(i, i + blockSize)); + binaryString = binaryString + binarySubString; + } + return binaryString; + }; + + var rules = getCSSRules(document), + source = embedCSSRulesInSVG(rules, svg), + data, + utf8BinaryString; + + source = patchSVGSource(source); + + // Encode the string as UTF-8 and convert it to a binary string. The UTF-8 encoding is required to + // capture unicode characters correctly. + utf8BinaryString = buildBinaryString(new (TextEncoder || TextEncoderLite)('utf-8').encode(source)); + + data = "data:image/svg+xml;base64," + btoa(utf8BinaryString); + img.src = data; + } + + function patchSVGSource(svgSource) { + var source = ''; + //add name spaces. + if (!svgSource.match(/^]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)) { + source = svgSource.replace(/^]+"http:\/\/www\.w3\.org\/1999\/xlink"/)) { + source = svgSource.replace(/^\r\n' + source; + } + + function copySVGToImg(svg, img) { + if (browser.isSafari() || browser.isMobileSafari()) { + copySVGToImgSafari(svg, img); + } else { + copySVGToImgMostBrowsers(svg, img); + } + } + + function adaptDestSizeToZoom(destinationCanvas, sources) { + function containsSVGs(source) { + return source.srcImgTagName === 'svg'; + } + + if (sources.find(containsSVGs) !== undefined) { + if (pixelRatio < 1) { + destinationCanvas.width = destinationCanvas.width * pixelRatio; + destinationCanvas.height = destinationCanvas.height * pixelRatio; + } + } + } + + function prepareImagesToBeComposed(sources, destination) { + var result = SUCCESSFULIMAGEPREPARATION; + if (sources.length === 0) { + result = EMPTYARRAYOFIMAGESOURCES; //nothing to do if called without sources + } else { + var minX = sources[0].genLeft; + var minY = sources[0].genTop; + var maxX = sources[0].genRight; + var maxY = sources[0].genBottom; + var i = 0; + + for (i = 1; i < sources.length; i++) { + if (minX > sources[i].genLeft) { + minX = sources[i].genLeft; + } + + if (minY > sources[i].genTop) { + minY = sources[i].genTop; + } + } + + for (i = 1; i < sources.length; i++) { + if (maxX < sources[i].genRight) { + maxX = sources[i].genRight; + } + + if (maxY < sources[i].genBottom) { + maxY = sources[i].genBottom; + } + } + + if ((maxX - minX <= 0) || (maxY - minY <= 0)) { + result = NEGATIVEIMAGESIZE; //this might occur on hidden images + } else { + destination.width = Math.round(maxX - minX); + destination.height = Math.round(maxY - minY); + + for (i = 0; i < sources.length; i++) { + sources[i].xCompOffset = sources[i].genLeft - minX; + sources[i].yCompOffset = sources[i].genTop - minY; + } + + adaptDestSizeToZoom(destination, sources); + } + } + return result; + } + + function copyImgsToCanvas(sources, destination) { + var prepareImagesResult = prepareImagesToBeComposed(sources, destination); + if (prepareImagesResult === SUCCESSFULIMAGEPREPARATION) { + var destinationCtx = destination.getContext('2d'); + + for (var i = 0; i < sources.length; i++) { + if (sources[i].successfullyLoaded === true) { + destinationCtx.drawImage(sources[i], sources[i].xCompOffset * pixelRatio, sources[i].yCompOffset * pixelRatio); + } + } + } + return prepareImagesResult; + } + + function adnotateDestImgWithBoundingClientRect(srcCanvasOrSvg, destImg) { + destImg.genLeft = srcCanvasOrSvg.getBoundingClientRect().left; + destImg.genTop = srcCanvasOrSvg.getBoundingClientRect().top; + + if (srcCanvasOrSvg.tagName === 'CANVAS') { + destImg.genRight = destImg.genLeft + srcCanvasOrSvg.width; + destImg.genBottom = destImg.genTop + srcCanvasOrSvg.height; + } + + if (srcCanvasOrSvg.tagName === 'svg') { + destImg.genRight = srcCanvasOrSvg.getBoundingClientRect().right; + destImg.genBottom = srcCanvasOrSvg.getBoundingClientRect().bottom; + } + } + + function generateTempImageFromCanvasOrSvg(srcCanvasOrSvg, destImg) { + if (srcCanvasOrSvg.tagName === 'CANVAS') { + copyCanvasToImg(srcCanvasOrSvg, destImg); + } + + if (srcCanvasOrSvg.tagName === 'svg') { + copySVGToImg(srcCanvasOrSvg, destImg); + } + + destImg.srcImgTagName = srcCanvasOrSvg.tagName; + adnotateDestImgWithBoundingClientRect(srcCanvasOrSvg, destImg); + } + + function failureCallback() { + return GENERALFAILURECALLBACKERROR; + } + + // used for testing + $.plot.composeImages = composeImages; + + function init(plot) { + // used to extend the public API of the plot + plot.composeImages = composeImages; + } + + $.plot.plugins.push({ + init: init, + name: 'composeImages', + version: '1.0' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.crosshair.js b/frontend/lib/flot/jquery.flot.crosshair.js new file mode 100644 index 0000000..385c705 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.crosshair.js @@ -0,0 +1,202 @@ +/* Flot plugin for showing crosshairs when the mouse hovers over the plot. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin supports these options: + + crosshair: { + mode: null or "x" or "y" or "xy" + color: color + lineWidth: number + } + +Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical +crosshair that lets you trace the values on the x axis, "y" enables a +horizontal crosshair and "xy" enables them both. "color" is the color of the +crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of +the drawn lines (default is 1). + +The plugin also adds four public methods: + + - setCrosshair( pos ) + + Set the position of the crosshair. Note that this is cleared if the user + moves the mouse. "pos" is in coordinates of the plot and should be on the + form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple + axes), which is coincidentally the same format as what you get from a + "plothover" event. If "pos" is null, the crosshair is cleared. + + - clearCrosshair() + + Clear the crosshair. + + - lockCrosshair(pos) + + Cause the crosshair to lock to the current location, no longer updating if + the user moves the mouse. Optionally supply a position (passed on to + setCrosshair()) to move it to. + + Example usage: + + var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; + $("#graph").bind( "plothover", function ( evt, position, item ) { + if ( item ) { + // Lock the crosshair to the data point being hovered + myFlot.lockCrosshair({ + x: item.datapoint[ 0 ], + y: item.datapoint[ 1 ] + }); + } else { + // Return normal crosshair operation + myFlot.unlockCrosshair(); + } + }); + + - unlockCrosshair() + + Free the crosshair to move again after locking it. +*/ + +(function ($) { + var options = { + crosshair: { + mode: null, // one of null, "x", "y" or "xy", + color: "rgba(170, 0, 0, 0.80)", + lineWidth: 1 + } + }; + + function init(plot) { + // position of crosshair in pixels + var crosshair = {x: -1, y: -1, locked: false, highlighted: false}; + + plot.setCrosshair = function setCrosshair(pos) { + if (!pos) { + crosshair.x = -1; + } else { + var o = plot.p2c(pos); + crosshair.x = Math.max(0, Math.min(o.left, plot.width())); + crosshair.y = Math.max(0, Math.min(o.top, plot.height())); + } + + plot.triggerRedrawOverlay(); + }; + + plot.clearCrosshair = plot.setCrosshair; // passes null for pos + + plot.lockCrosshair = function lockCrosshair(pos) { + if (pos) { + plot.setCrosshair(pos); + } + + crosshair.locked = true; + }; + + plot.unlockCrosshair = function unlockCrosshair() { + crosshair.locked = false; + crosshair.rect = null; + }; + + function onMouseOut(e) { + if (crosshair.locked) { + return; + } + + if (crosshair.x !== -1) { + crosshair.x = -1; + plot.triggerRedrawOverlay(); + } + } + + function onMouseMove(e) { + var offset = plot.offset(); + if (crosshair.locked) { + var mouseX = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); + var mouseY = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); + + if ((mouseX > crosshair.x - 4) && (mouseX < crosshair.x + 4) && (mouseY > crosshair.y - 4) && (mouseY < crosshair.y + 4)) { + if (!crosshair.highlighted) { + crosshair.highlighted = true; + plot.triggerRedrawOverlay(); + } + } else { + if (crosshair.highlighted) { + crosshair.highlighted = false; + plot.triggerRedrawOverlay(); + } + } + return; + } + + if (plot.getSelection && plot.getSelection()) { + crosshair.x = -1; // hide the crosshair while selecting + return; + } + + crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); + crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); + plot.triggerRedrawOverlay(); + } + + plot.hooks.bindEvents.push(function (plot, eventHolder) { + if (!plot.getOptions().crosshair.mode) { + return; + } + + eventHolder.mouseout(onMouseOut); + eventHolder.mousemove(onMouseMove); + }); + + plot.hooks.drawOverlay.push(function (plot, ctx) { + var c = plot.getOptions().crosshair; + if (!c.mode) { + return; + } + + var plotOffset = plot.getPlotOffset(); + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + if (crosshair.x !== -1) { + var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0; + + ctx.strokeStyle = c.color; + ctx.lineWidth = c.lineWidth; + ctx.lineJoin = "round"; + + ctx.beginPath(); + if (c.mode.indexOf("x") !== -1) { + var drawX = Math.floor(crosshair.x) + adj; + ctx.moveTo(drawX, 0); + ctx.lineTo(drawX, plot.height()); + } + if (c.mode.indexOf("y") !== -1) { + var drawY = Math.floor(crosshair.y) + adj; + ctx.moveTo(0, drawY); + ctx.lineTo(plot.width(), drawY); + } + if (crosshair.locked) { + if (crosshair.highlighted) ctx.fillStyle = 'orange'; + else ctx.fillStyle = c.color; + ctx.fillRect(Math.floor(crosshair.x) + adj - 4, Math.floor(crosshair.y) + adj - 4, 8, 8); + } + ctx.stroke(); + } + ctx.restore(); + }); + + plot.hooks.shutdown.push(function (plot, eventHolder) { + eventHolder.unbind("mouseout", onMouseOut); + eventHolder.unbind("mousemove", onMouseMove); + }); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'crosshair', + version: '1.0' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.drawSeries.js b/frontend/lib/flot/jquery.flot.drawSeries.js new file mode 100644 index 0000000..bf1272f --- /dev/null +++ b/frontend/lib/flot/jquery.flot.drawSeries.js @@ -0,0 +1,663 @@ +/** +## jquery.flot.drawSeries.js + +This plugin is used by flot for drawing lines, plots, bars or area. + +### Public methods +*/ + +(function($) { + "use strict"; + + function DrawSeries() { + function plotLine(datapoints, xoffset, yoffset, axisx, axisy, ctx, steps) { + var points = datapoints.points, + ps = datapoints.pointsize, + prevx = null, + prevy = null; + var x1 = 0.0, + y1 = 0.0, + x2 = 0.0, + y2 = 0.0, + mx = null, + my = null, + i = 0; + + ctx.beginPath(); + for (i = ps; i < points.length; i += ps) { + x1 = points[i - ps]; + y1 = points[i - ps + 1]; + x2 = points[i]; + y2 = points[i + 1]; + + if (x1 === null || x2 === null) { + mx = null; + my = null; + continue; + } + + if (isNaN(x1) || isNaN(x2) || isNaN(y1) || isNaN(y2)) { + prevx = null; + prevy = null; + continue; + } + + if(steps){ + if (mx !== null && my !== null) { + // if middle point exists, transfer p2 -> p1 and p1 -> mp + x2 = x1; + y2 = y1; + x1 = mx; + y1 = my; + + // 'remove' middle point + mx = null; + my = null; + + // subtract pointsize from i to have current point p1 handled again + i -= ps; + } else if (y1 !== y2 && x1 !== x2) { + // create a middle point + y2 = y1; + mx = x2; + my = y1; + } + } + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min) { + if (y2 < axisy.min) { + // line segment is outside + continue; + } + // 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) { + 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) { + continue; + } + + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + 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) { + continue; + } + + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + 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) { + continue; + } + + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + 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) { + ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); + } + + prevx = x2; + prevy = y2; + ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); + } + ctx.stroke(); + } + + function plotLineArea(datapoints, axisx, axisy, fillTowards, ctx, steps) { + var points = datapoints.points, + ps = datapoints.pointsize, + bottom = fillTowards > axisy.min ? Math.min(axisy.max, fillTowards) : axisy.min, + i = 0, + ypos = 1, + areaOpen = false, + segmentStart = 0, + segmentEnd = 0, + mx = null, + my = null; + + // 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) { + break; + } + + i += ps; // ps is negative if going backwards + + var x1 = points[i - ps], + y1 = points[i - ps + ypos], + x2 = points[i], + y2 = points[i + ypos]; + + if (ps === -2) { + /* going backwards and no value for the bottom provided in the series*/ + y1 = y2 = bottom; + } + + if (areaOpen) { + if (ps > 0 && x1 != null && x2 == null) { + // at turning point + segmentEnd = i; + ps = -ps; + ypos = 2; + continue; + } + + if (ps < 0 && i === segmentStart + ps) { + // done with the reverse sweep + ctx.fill(); + areaOpen = false; + ps = -ps; + ypos = 1; + i = segmentStart = segmentEnd + ps; + continue; + } + } + + if (x1 == null || x2 == null) { + mx = null; + my = null; + continue; + } + + if(steps){ + if (mx !== null && my !== null) { + // if middle point exists, transfer p2 -> p1 and p1 -> mp + x2 = x1; + y2 = y1; + x1 = mx; + y1 = my; + + // 'remove' middle point + mx = null; + my = null; + + // subtract pointsize from i to have current point p1 handled again + i -= ps; + } else if (y1 !== y2 && x1 !== x2) { + // create a middle point + y2 = y1; + mx = x2; + my = y1; + } + } + + // clip x values + + // clip with xmin + if (x1 <= x2 && x1 < 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) { + 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) { + continue; + } + + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + 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 (!areaOpen) { + // open area + ctx.beginPath(); + ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); + areaOpen = true; + } + + // now first check the case where both is outside + if (y1 >= axisy.max && y2 >= axisy.max) { + 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) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); + continue; + } + + // else it's a bit more complicated, there might + // be a flat maxed out rectangle first, then a + // triangular cutout or reverse; to find these + // keep track of the current x values + var x1old = x1, + x2old = x2; + + // clip the y values, without shortcutting, we + // go through all cases in turn + + // clip with ymin + 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) { + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + 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) { + 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) { + ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); + // it goes to (x1, y1), but we fill that below + } + + // fill triangular section, this sometimes result + // in redundant points if (x1, y1) hasn't changed + // from previous line to, but we just ignore that + ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + + // fill the other rectangle if it's there + if (x2 !== x2old) { + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); + } + } + } + + /** + - drawSeriesLines(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) + + This function is used for drawing lines or area fill. In case the series has line decimation function + attached, before starting to draw, as an optimization the points will first be decimated. + + The series parameter contains the series to be drawn on ctx context. The plotOffset, plotWidth and + plotHeight are the corresponding parameters of flot used to determine the drawing surface. + The function getColorOrGradient is used to compute the fill style of lines and area. + */ + function drawSeriesLines(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) { + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + ctx.lineJoin = "round"; + + if (series.lines.dashes && ctx.setLineDash) { + ctx.setLineDash(series.lines.dashes); + } + + var datapoints = { + format: series.datapoints.format, + points: series.datapoints.points, + pointsize: series.datapoints.pointsize + }; + + if (series.decimate) { + datapoints.points = series.decimate(series, series.xaxis.min, series.xaxis.max, plotWidth, series.yaxis.min, series.yaxis.max, plotHeight); + } + + var lw = series.lines.lineWidth; + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight, getColorOrGradient); + if (fillStyle) { + ctx.fillStyle = fillStyle; + plotLineArea(datapoints, series.xaxis, series.yaxis, series.lines.fillTowards || 0, ctx, series.lines.steps); + } + + if (lw > 0) { + plotLine(datapoints, 0, 0, series.xaxis, series.yaxis, ctx, series.lines.steps); + } + + ctx.restore(); + } + + /** + - drawSeriesPoints(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) + + This function is used for drawing points using a given symbol. In case the series has points decimation + function attached, before starting to draw, as an optimization the points will first be decimated. + + The series parameter contains the series to be drawn on ctx context. The plotOffset, plotWidth and + plotHeight are the corresponding parameters of flot used to determine the drawing surface. + The function drawSymbol is used to compute and draw the symbol chosen for the points. + */ + function drawSeriesPoints(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) { + function drawCircle(ctx, x, y, radius, shadow, fill) { + ctx.moveTo(x + radius, y); + ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); + } + drawCircle.fill = true; + function plotPoints(datapoints, radius, fill, offset, shadow, axisx, axisy, drawSymbolFn) { + var points = datapoints.points, + ps = datapoints.pointsize; + + ctx.beginPath(); + 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) { + continue; + } + + x = axisx.p2c(x); + y = axisy.p2c(y) + offset; + + drawSymbolFn(ctx, x, y, radius, shadow, fill); + } + if (drawSymbolFn.fill && !shadow) { + ctx.fill(); + } + ctx.stroke(); + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var datapoints = { + format: series.datapoints.format, + points: series.datapoints.points, + pointsize: series.datapoints.pointsize + }; + + if (series.decimatePoints) { + datapoints.points = series.decimatePoints(series, series.xaxis.min, series.xaxis.max, plotWidth, series.yaxis.min, series.yaxis.max, plotHeight); + } + + var lw = series.points.lineWidth, + radius = series.points.radius, + symbol = series.points.symbol, + drawSymbolFn; + + if (symbol === 'circle') { + drawSymbolFn = drawCircle; + } else if (typeof symbol === 'string' && drawSymbol && drawSymbol[symbol]) { + drawSymbolFn = drawSymbol[symbol]; + } else if (typeof drawSymbol === 'function') { + drawSymbolFn = drawSymbol; + } + + // If the user sets the line width to 0, we change it to a very + // small value. A line width of 0 seems to force the default of 1. + + if (lw === 0) { + lw = 0.0001; + } + + ctx.lineWidth = lw; + ctx.fillStyle = getFillStyle(series.points, series.color, null, null, getColorOrGradient); + ctx.strokeStyle = series.color; + plotPoints(datapoints, radius, + true, 0, false, + series.xaxis, series.yaxis, drawSymbolFn); + ctx.restore(); + } + + function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { + var left = x + barLeft, + right = x + barRight, + bottom = b, top = y, + drawLeft, drawRight, drawTop, drawBottom = false, + tmp; + + drawLeft = drawRight = drawTop = true; + + // in horizontal mode, we start the bar from the left + // instead of from the bottom so it appears to be + // horizontal rather than vertical + if (horizontal) { + drawBottom = drawRight = drawTop = true; + drawLeft = false; + left = b; + right = x; + top = y + barLeft; + bottom = y + barRight; + + // account for negative bars + if (right < left) { + tmp = right; + right = left; + left = tmp; + drawLeft = true; + drawRight = false; + } + } + else { + drawLeft = drawRight = drawTop = true; + drawBottom = false; + left = x + barLeft; + right = x + barRight; + bottom = b; + top = y; + + // account for negative bars + if (top < bottom) { + tmp = top; + top = bottom; + bottom = tmp; + drawBottom = true; + drawTop = false; + } + } + + // clip + if (right < axisx.min || left > axisx.max || + top < axisy.min || bottom > axisy.max) { + return; + } + + if (left < axisx.min) { + left = axisx.min; + drawLeft = false; + } + + if (right > axisx.max) { + right = axisx.max; + drawRight = false; + } + + if (bottom < axisy.min) { + bottom = axisy.min; + drawBottom = false; + } + + if (top > axisy.max) { + top = axisy.max; + drawTop = false; + } + + left = axisx.p2c(left); + bottom = axisy.p2c(bottom); + right = axisx.p2c(right); + top = axisy.p2c(top); + + // fill the bar + if (fillStyleCallback) { + c.fillStyle = fillStyleCallback(bottom, top); + c.fillRect(left, top, right - left, bottom - top) + } + + // draw outline + if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { + 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.stroke(); + } + } + + /** + - drawSeriesBars(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) + + This function is used for drawing series represented as bars. In case the series has decimation + function attached, before starting to draw, as an optimization the points will first be decimated. + + The series parameter contains the series to be drawn on ctx context. The plotOffset, plotWidth and + plotHeight are the corresponding parameters of flot used to determine the drawing surface. + The function getColorOrGradient is used to compute the fill style of bars. + */ + function drawSeriesBars(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) { + function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + fillTowards = series.bars.fillTowards || 0, + defaultBottom = fillTowards > axisy.min ? Math.min(axisy.max, fillTowards) : axisy.min; + + for (var i = 0; i < points.length; i += ps) { + if (points[i] == null) { + continue; + } + + // Use third point as bottom if pointsize is 3 + var bottom = ps === 3 ? points[i + 2] : defaultBottom; + drawBar(points[i], points[i + 1], bottom, barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var datapoints = { + format: series.datapoints.format, + points: series.datapoints.points, + pointsize: series.datapoints.pointsize + }; + + if (series.decimate) { + datapoints.points = series.decimate(series, series.xaxis.min, series.xaxis.max, plotWidth); + } + + ctx.lineWidth = series.bars.lineWidth; + ctx.strokeStyle = series.color; + + var barLeft; + var barWidth = series.bars.barWidth[0] || series.bars.barWidth; + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -barWidth; + break; + default: + barLeft = -barWidth / 2; + } + + var fillStyleCallback = series.bars.fill ? function(bottom, top) { + return getFillStyle(series.bars, series.color, bottom, top, getColorOrGradient); + } : null; + + plotBars(datapoints, barLeft, barLeft + barWidth, fillStyleCallback, series.xaxis, series.yaxis); + ctx.restore(); + } + + function getFillStyle(filloptions, seriesColor, bottom, top, getColorOrGradient) { + var fill = filloptions.fill; + if (!fill) { + return null; + } + + if (filloptions.fillColor) { + return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); + } + + var c = $.color.parse(seriesColor); + c.a = typeof fill === "number" ? fill : 0.4; + c.normalize(); + return c.toString(); + } + + this.drawSeriesLines = drawSeriesLines; + this.drawSeriesPoints = drawSeriesPoints; + this.drawSeriesBars = drawSeriesBars; + this.drawBar = drawBar; + }; + + $.plot.drawSeries = new DrawSeries(); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.errorbars.js b/frontend/lib/flot/jquery.flot.errorbars.js new file mode 100644 index 0000000..956562e --- /dev/null +++ b/frontend/lib/flot/jquery.flot.errorbars.js @@ -0,0 +1,375 @@ +/* Flot plugin for plotting error bars. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +Error bars are used to show standard deviation and other statistical +properties in a plot. + +* Created by Rui Pereira - rui (dot) pereira (at) gmail (dot) com + +This plugin allows you to plot error-bars over points. Set "errorbars" inside +the points series to the axis name over which there will be error values in +your data array (*even* if you do not intend to plot them later, by setting +"show: null" on xerr/yerr). + +The plugin supports these options: + + series: { + points: { + errorbars: "x" or "y" or "xy", + xerr: { + show: null/false or true, + asymmetric: null/false or true, + upperCap: null or "-" or function, + lowerCap: null or "-" or function, + color: null or color, + radius: null or number + }, + yerr: { same options as xerr } + } + } + +Each data point array is expected to be of the type: + + "x" [ x, y, xerr ] + "y" [ x, y, yerr ] + "xy" [ x, y, xerr, yerr ] + +Where xerr becomes xerr_lower,xerr_upper for the asymmetric error case, and +equivalently for yerr. Eg., a datapoint for the "xy" case with symmetric +error-bars on X and asymmetric on Y would be: + + [ x, y, xerr, yerr_lower, yerr_upper ] + +By default no end caps are drawn. Setting upperCap and/or lowerCap to "-" will +draw a small cap perpendicular to the error bar. They can also be set to a +user-defined drawing function, with (ctx, x, y, radius) as parameters, as eg. + + function drawSemiCircle( ctx, x, y, radius ) { + ctx.beginPath(); + ctx.arc( x, y, radius, 0, Math.PI, false ); + ctx.moveTo( x - radius, y ); + ctx.lineTo( x + radius, y ); + ctx.stroke(); + } + +Color and radius both default to the same ones of the points series if not +set. The independent radius parameter on xerr/yerr is useful for the case when +we may want to add error-bars to a line, without showing the interconnecting +points (with radius: 0), and still showing end caps on the error-bars. +shadowSize and lineWidth are derived as well from the points series. + +*/ + +(function ($) { + var options = { + series: { + points: { + errorbars: null, //should be 'x', 'y' or 'xy' + xerr: {err: 'x', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null}, + yerr: {err: 'y', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null} + } + } + }; + + function processRawData(plot, series, data, datapoints) { + if (!series.points.errorbars) { + return; + } + + // x,y values + var format = [ + { x: true, number: true, required: true }, + { y: true, number: true, required: true } + ]; + + var errors = series.points.errorbars; + // error bars - first X then Y + if (errors === 'x' || errors === 'xy') { + // lower / upper error + if (series.points.xerr.asymmetric) { + format.push({ x: true, number: true, required: true }); + format.push({ x: true, number: true, required: true }); + } else { + format.push({ x: true, number: true, required: true }); + } + } + if (errors === 'y' || errors === 'xy') { + // lower / upper error + if (series.points.yerr.asymmetric) { + format.push({ y: true, number: true, required: true }); + format.push({ y: true, number: true, required: true }); + } else { + format.push({ y: true, number: true, required: true }); + } + } + datapoints.format = format; + } + + function parseErrors(series, i) { + var points = series.datapoints.points; + + // read errors from points array + var exl = null, + exu = null, + eyl = null, + eyu = null; + var xerr = series.points.xerr, + yerr = series.points.yerr; + + var eb = series.points.errorbars; + // error bars - first X + if (eb === 'x' || eb === 'xy') { + if (xerr.asymmetric) { + exl = points[i + 2]; + exu = points[i + 3]; + if (eb === 'xy') { + if (yerr.asymmetric) { + eyl = points[i + 4]; + eyu = points[i + 5]; + } else { + eyl = points[i + 4]; + } + } + } else { + exl = points[i + 2]; + if (eb === 'xy') { + if (yerr.asymmetric) { + eyl = points[i + 3]; + eyu = points[i + 4]; + } else { + eyl = points[i + 3]; + } + } + } + // only Y + } else { + if (eb === 'y') { + if (yerr.asymmetric) { + eyl = points[i + 2]; + eyu = points[i + 3]; + } else { + eyl = points[i + 2]; + } + } + } + + // symmetric errors? + if (exu == null) exu = exl; + if (eyu == null) eyu = eyl; + + var errRanges = [exl, exu, eyl, eyu]; + // nullify if not showing + if (!xerr.show) { + errRanges[0] = null; + errRanges[1] = null; + } + if (!yerr.show) { + errRanges[2] = null; + errRanges[3] = null; + } + return errRanges; + } + + function drawSeriesErrors(plot, ctx, s) { + var points = s.datapoints.points, + ps = s.datapoints.pointsize, + ax = [s.xaxis, s.yaxis], + radius = s.points.radius, + err = [s.points.xerr, s.points.yerr], + tmp; + + //sanity check, in case some inverted axis hack is applied to flot + var invertX = false; + if (ax[0].p2c(ax[0].max) < ax[0].p2c(ax[0].min)) { + invertX = true; + tmp = err[0].lowerCap; + err[0].lowerCap = err[0].upperCap; + err[0].upperCap = tmp; + } + + var invertY = false; + if (ax[1].p2c(ax[1].min) < ax[1].p2c(ax[1].max)) { + invertY = true; + tmp = err[1].lowerCap; + err[1].lowerCap = err[1].upperCap; + err[1].upperCap = tmp; + } + + for (var i = 0; i < s.datapoints.points.length; i += ps) { + //parse + var errRanges = parseErrors(s, i); + + //cycle xerr & yerr + for (var e = 0; e < err.length; e++) { + var minmax = [ax[e].min, ax[e].max]; + + //draw this error? + if (errRanges[e * err.length]) { + //data coordinates + var x = points[i], + y = points[i + 1]; + + //errorbar ranges + var upper = [x, y][e] + errRanges[e * err.length + 1], + lower = [x, y][e] - errRanges[e * err.length]; + + //points outside of the canvas + if (err[e].err === 'x') { + if (y > ax[1].max || y < ax[1].min || upper < ax[0].min || lower > ax[0].max) { + continue; + } + } + + if (err[e].err === 'y') { + if (x > ax[0].max || x < ax[0].min || upper < ax[1].min || lower > ax[1].max) { + continue; + } + } + + // prevent errorbars getting out of the canvas + var drawUpper = true, + drawLower = true; + + if (upper > minmax[1]) { + drawUpper = false; + upper = minmax[1]; + } + if (lower < minmax[0]) { + drawLower = false; + lower = minmax[0]; + } + + //sanity check, in case some inverted axis hack is applied to flot + if ((err[e].err === 'x' && invertX) || (err[e].err === 'y' && invertY)) { + //swap coordinates + tmp = lower; + lower = upper; + upper = tmp; + tmp = drawLower; + drawLower = drawUpper; + drawUpper = tmp; + tmp = minmax[0]; + minmax[0] = minmax[1]; + minmax[1] = tmp; + } + + // convert to pixels + x = ax[0].p2c(x); + y = ax[1].p2c(y); + upper = ax[e].p2c(upper); + lower = ax[e].p2c(lower); + minmax[0] = ax[e].p2c(minmax[0]); + minmax[1] = ax[e].p2c(minmax[1]); + + //same style as points by default + var lw = err[e].lineWidth ? err[e].lineWidth : s.points.lineWidth, + sw = s.points.shadowSize != null ? s.points.shadowSize : s.shadowSize; + + //shadow as for points + if (lw > 0 && sw > 0) { + var w = sw / 2; + ctx.lineWidth = w; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w + w / 2, minmax); + + ctx.strokeStyle = "rgba(0,0,0,0.2)"; + drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w / 2, minmax); + } + + ctx.strokeStyle = err[e].color + ? err[e].color + : s.color; + ctx.lineWidth = lw; + //draw it + drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, 0, minmax); + } + } + } + } + + function drawError(ctx, err, x, y, upper, lower, drawUpper, drawLower, radius, offset, minmax) { + //shadow offset + y += offset; + upper += offset; + lower += offset; + + // error bar - avoid plotting over circles + if (err.err === 'x') { + if (upper > x + radius) drawPath(ctx, [[upper, y], [Math.max(x + radius, minmax[0]), y]]); + else drawUpper = false; + + if (lower < x - radius) drawPath(ctx, [[Math.min(x - radius, minmax[1]), y], [lower, y]]); + else drawLower = false; + } else { + if (upper < y - radius) drawPath(ctx, [[x, upper], [x, Math.min(y - radius, minmax[0])]]); + else drawUpper = false; + + if (lower > y + radius) drawPath(ctx, [[x, Math.max(y + radius, minmax[1])], [x, lower]]); + else drawLower = false; + } + + //internal radius value in errorbar, allows to plot radius 0 points and still keep proper sized caps + //this is a way to get errorbars on lines without visible connecting dots + radius = err.radius != null + ? err.radius + : radius; + + // upper cap + if (drawUpper) { + if (err.upperCap === '-') { + if (err.err === 'x') drawPath(ctx, [[upper, y - radius], [upper, y + radius]]); + else drawPath(ctx, [[x - radius, upper], [x + radius, upper]]); + } else if ($.isFunction(err.upperCap)) { + if (err.err === 'x') err.upperCap(ctx, upper, y, radius); + else err.upperCap(ctx, x, upper, radius); + } + } + // lower cap + if (drawLower) { + if (err.lowerCap === '-') { + if (err.err === 'x') drawPath(ctx, [[lower, y - radius], [lower, y + radius]]); + else drawPath(ctx, [[x - radius, lower], [x + radius, lower]]); + } else if ($.isFunction(err.lowerCap)) { + if (err.err === 'x') err.lowerCap(ctx, lower, y, radius); + else err.lowerCap(ctx, x, lower, radius); + } + } + } + + function drawPath(ctx, pts) { + ctx.beginPath(); + ctx.moveTo(pts[0][0], pts[0][1]); + for (var p = 1; p < pts.length; p++) { + ctx.lineTo(pts[p][0], pts[p][1]); + } + + ctx.stroke(); + } + + function draw(plot, ctx) { + var plotOffset = plot.getPlotOffset(); + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + $.each(plot.getData(), function (i, s) { + if (s.points.errorbars && (s.points.xerr.show || s.points.yerr.show)) { + drawSeriesErrors(plot, ctx, s); + } + }); + ctx.restore(); + } + + function init(plot) { + plot.hooks.processRawData.push(processRawData); + plot.hooks.draw.push(draw); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'errorbars', + version: '1.0' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.fillbetween.js b/frontend/lib/flot/jquery.flot.fillbetween.js new file mode 100644 index 0000000..96cb292 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.fillbetween.js @@ -0,0 +1,254 @@ +/* Flot plugin for computing bottoms for filled line and bar charts. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The case: you've got two series that you want to fill the area between. In Flot +terms, you need to use one as the fill bottom of the other. You can specify the +bottom of each data point as the third coordinate manually, or you can use this +plugin to compute it for you. + +In order to name the other series, you need to give it an id, like this: + + var dataset = [ + { data: [ ... ], id: "foo" } , // use default bottom + { data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom + ]; + + $.plot($("#placeholder"), dataset, { lines: { show: true, fill: true }}); + +As a convenience, if the id given is a number that doesn't appear as an id in +the series, it is interpreted as the index in the array instead (so fillBetween: +0 can also mean the first series). + +Internally, the plugin modifies the datapoints in each series. For line series, +extra data points might be inserted through interpolation. Note that at points +where the bottom line is not defined (due to a null point or start/end of line), +the current line will show a gap too. The algorithm comes from the +jquery.flot.stack.js plugin, possibly some code could be shared. + +*/ + +(function ($) { + var options = { + series: { + fillBetween: null // or number + } + }; + + function init(plot) { + function findBottomSeries(s, allseries) { + var i; + + for (i = 0; i < allseries.length; ++i) { + if (allseries[ i ].id === s.fillBetween) { + return allseries[ i ]; + } + } + + if (typeof s.fillBetween === "number") { + if (s.fillBetween < 0 || s.fillBetween >= allseries.length) { + return null; + } + return allseries[ s.fillBetween ]; + } + + return null; + } + + function computeFormat(plot, s, data, datapoints) { + if (s.fillBetween == null) { + return; + } + + format = datapoints.format; + var plotHasId = function(id) { + var plotData = plot.getData(); + for (i = 0; i < plotData.length; i++) { + if (plotData[i].id === id) { + return true; + } + } + + return false; + } + + if (!format) { + format = []; + + format.push({ + x: true, + number: true, + computeRange: s.xaxis.options.autoScale !== 'none', + required: true + }); + format.push({ + y: true, + number: true, + computeRange: s.yaxis.options.autoScale !== 'none', + required: true + }); + + if (s.fillBetween !== undefined && s.fillBetween !== '' && plotHasId(s.fillBetween) && s.fillBetween !== s.id) { + format.push({ + x: false, + y: true, + number: true, + required: false, + computeRange: s.yaxis.options.autoScale !== 'none', + defaultValue: 0 + }); + } + + datapoints.format = format; + } + } + + function computeFillBottoms(plot, s, datapoints) { + if (s.fillBetween == null) { + return; + } + + var other = findBottomSeries(s, plot.getData()); + + if (!other) { + return; + } + + var ps = datapoints.pointsize, + points = datapoints.points, + otherps = other.datapoints.pointsize, + otherpoints = other.datapoints.points, + newpoints = [], + px, py, intery, qx, qy, bottom, + withlines = s.lines.show, + withbottom = ps > 2 && datapoints.format[2].y, + withsteps = withlines && s.lines.steps, + fromgap = true, + i = 0, + j = 0, + l, m; + + while (true) { + if (i >= points.length) { + break; + } + + l = newpoints.length; + + if (points[ i ] == null) { + // copy gaps + for (m = 0; m < ps; ++m) { + newpoints.push(points[ i + m ]); + } + + i += ps; + } else if (j >= otherpoints.length) { + // for lines, we can't use the rest of the points + if (!withlines) { + for (m = 0; m < ps; ++m) { + newpoints.push(points[ i + m ]); + } + } + + i += ps; + } else if (otherpoints[ j ] == null) { + // oops, got a gap + for (m = 0; m < ps; ++m) { + newpoints.push(null); + } + + fromgap = true; + j += otherps; + } else { + // cases where we actually got two points + px = points[ i ]; + py = points[ i + 1 ]; + qx = otherpoints[ j ]; + qy = otherpoints[ j + 1 ]; + bottom = 0; + + if (px === qx) { + for (m = 0; m < ps; ++m) { + newpoints.push(points[ i + m ]); + } + + //newpoints[ l + 1 ] += qy; + bottom = qy; + + i += ps; + j += otherps; + } else if (px > qx) { + // we got past point below, might need to + // insert interpolated extra point + + if (withlines && i > 0 && points[ i - ps ] != null) { + intery = py + (points[ i - ps + 1 ] - py) * (qx - px) / (points[ i - ps ] - px); + newpoints.push(qx); + newpoints.push(intery); + for (m = 2; m < ps; ++m) { + newpoints.push(points[ i + m ]); + } + bottom = qy; + } + + j += otherps; + } else { + // px < qx + // if we come from a gap, we just skip this point + + if (fromgap && withlines) { + i += ps; + continue; + } + + for (m = 0; m < ps; ++m) { + newpoints.push(points[ i + m ]); + } + + // we might be able to interpolate a point below, + // this can give us a better y + + if (withlines && j > 0 && otherpoints[ j - otherps ] != null) { + bottom = qy + (otherpoints[ j - otherps + 1 ] - qy) * (px - qx) / (otherpoints[ j - otherps ] - qx); + } + + //newpoints[l + 1] += bottom; + + i += ps; + } + + fromgap = false; + + if (l !== newpoints.length && withbottom) { + newpoints[ l + 2 ] = bottom; + } + } + + // maintain the line steps invariant + + if (withsteps && l !== newpoints.length && l > 0 && + newpoints[ l ] !== null && + newpoints[ l ] !== newpoints[ l - ps ] && + newpoints[ l + 1 ] !== newpoints[ l - ps + 1 ]) { + for (m = 0; m < ps; ++m) { + newpoints[ l + ps + m ] = newpoints[ l + m ]; + } + newpoints[ l + 1 ] = newpoints[ l - ps + 1 ]; + } + } + + datapoints.points = newpoints; + } + + plot.hooks.processRawData.push(computeFormat); + plot.hooks.processDatapoints.push(computeFillBottoms); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: "fillbetween", + version: "1.0" + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.flatdata.js b/frontend/lib/flot/jquery.flot.flatdata.js new file mode 100644 index 0000000..b91d168 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.flatdata.js @@ -0,0 +1,47 @@ +/* Support for flat 1D data series. + +A 1D flat data series is a data series in the form of a regular 1D array. The +main reason for using a flat data series is that it performs better, consumes +less memory and generates less garbage collection than the regular flot format. + +Example: + + plot.setData([[[0,0], [1,1], [2,2], [3,3]]]); // regular flot format + plot.setData([{flatdata: true, data: [0, 1, 2, 3]}]); // flatdata format + +Set series.flatdata to true to enable this plugin. + +You can use series.start to specify the starting index of the series (default is 0) +You can use series.step to specify the interval between consecutive indexes of the series (default is 1) +*/ + +/* global jQuery*/ + +(function ($) { + 'use strict'; + + function process1DRawData(plot, series, data, datapoints) { + if (series.flatdata === true) { + var start = series.start || 0; + var step = typeof series.step === 'number' ? series.step : 1; + datapoints.pointsize = 2; + for (var i = 0, j = 0; i < data.length; i++, j += 2) { + datapoints.points[j] = start + (i * step); + datapoints.points[j + 1] = data[i]; + } + if (datapoints.points !== undefined) { + datapoints.points.length = data.length * 2; + } else { + datapoints.points = []; + } + } + } + + $.plot.plugins.push({ + init: function(plot) { + plot.hooks.processRawData.push(process1DRawData); + }, + name: 'flatdata', + version: '0.0.2' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.hover.js b/frontend/lib/flot/jquery.flot.hover.js new file mode 100644 index 0000000..fadea8f --- /dev/null +++ b/frontend/lib/flot/jquery.flot.hover.js @@ -0,0 +1,350 @@ +/* global jQuery */ + +/** +## jquery.flot.hover.js + +This plugin is used for mouse hover and tap on a point of plot series. +It supports the following options: +```js +grid: { + hoverable: false, //to trigger plothover event on mouse hover or tap on a point + clickable: false //to trigger plotclick event on mouse hover +} +``` + +It listens to native mouse move event or click, as well as artificial generated +tap and touchevent. + +When the mouse is over a point or a tap on a point is performed, that point or +the correscponding bar will be highlighted and a "plothover" event will be generated. + +Custom "touchevent" is triggered when any touch interaction is made. Hover plugin +handles this events by unhighlighting all of the previously highlighted points and generates +"plothovercleanup" event to notify any part that is handling plothover (for exemple to cleanup +the tooltip from webcharts). +*/ + +(function($) { + 'use strict'; + + var options = { + grid: { + hoverable: false, + clickable: false + } + }; + + var browser = $.plot.browser; + + var eventType = { + click: 'click', + hover: 'hover' + } + + function init(plot) { + var lastMouseMoveEvent; + var highlights = []; + + function bindEvents(plot, eventHolder) { + var o = plot.getOptions(); + + if (o.grid.hoverable || o.grid.clickable) { + eventHolder[0].addEventListener('touchevent', triggerCleanupEvent, false); + eventHolder[0].addEventListener('tap', generatePlothoverEvent, false); + } + + if (o.grid.clickable) { + eventHolder.bind("click", onClick); + } + + if (o.grid.hoverable) { + eventHolder.bind("mousemove", onMouseMove); + + // Use bind, rather than .mouseleave, because we officially + // still support jQuery 1.2.6, which doesn't define a shortcut + // for mouseenter or mouseleave. This was a bug/oversight that + // was fixed somewhere around 1.3.x. We can return to using + // .mouseleave when we drop support for 1.2.6. + + eventHolder.bind("mouseleave", onMouseLeave); + } + } + + function shutdown(plot, eventHolder) { + eventHolder[0].removeEventListener('tap', generatePlothoverEvent); + eventHolder[0].removeEventListener('touchevent', triggerCleanupEvent); + eventHolder.unbind("mousemove", onMouseMove); + eventHolder.unbind("mouseleave", onMouseLeave); + eventHolder.unbind("click", onClick); + highlights = []; + } + + + function generatePlothoverEvent(e) { + var o = plot.getOptions(), + newEvent = new CustomEvent('mouseevent'); + + //transform from touch event to mouse event format + newEvent.pageX = e.detail.changedTouches[0].pageX; + newEvent.pageY = e.detail.changedTouches[0].pageY; + newEvent.clientX = e.detail.changedTouches[0].clientX; + newEvent.clientY = e.detail.changedTouches[0].clientY; + + if (o.grid.hoverable) { + doTriggerClickHoverEvent(newEvent, eventType.hover, 30); + } + return false; + } + + function doTriggerClickHoverEvent(event, eventType, searchDistance) { + var series = plot.getData(); + if (event !== undefined + && series.length > 0 + && series[0].xaxis.c2p !== undefined + && series[0].yaxis.c2p !== undefined) { + var eventToTrigger = "plot" + eventType; + var seriesFlag = eventType + "able"; + triggerClickHoverEvent(eventToTrigger, event, + function(i) { + return series[i][seriesFlag] !== false; + }, searchDistance); + } + } + + function onMouseMove(e) { + lastMouseMoveEvent = e; + plot.getPlaceholder()[0].lastMouseMoveEvent = e; + doTriggerClickHoverEvent(e, eventType.hover); + } + + function onMouseLeave(e) { + lastMouseMoveEvent = undefined; + plot.getPlaceholder()[0].lastMouseMoveEvent = undefined; + triggerClickHoverEvent("plothover", e, + function(i) { + return false; + }); + } + + function onClick(e) { + doTriggerClickHoverEvent(e, eventType.click); + } + + function triggerCleanupEvent() { + plot.unhighlight(); + plot.getPlaceholder().trigger('plothovercleanup'); + } + + // trigger click or hover event (they send the same parameters + // so we share their code) + function triggerClickHoverEvent(eventname, event, seriesFilter, searchDistance) { + var options = plot.getOptions(), + offset = plot.offset(), + page = browser.getPageXY(event), + canvasX = page.X - offset.left, + canvasY = page.Y - offset.top, + pos = plot.c2p({ + left: canvasX, + top: canvasY + }), + distance = searchDistance !== undefined ? searchDistance : options.grid.mouseActiveRadius; + + pos.pageX = page.X; + pos.pageY = page.Y; + + var item = plot.findNearbyItem(canvasX, canvasY, seriesFilter, distance); + + if (item) { + // fill in mouse pos for any listeners out there + item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left, 10); + item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top, 10); + } + + if (options.grid.autoHighlight) { + // 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])) || !item) { + unhighlight(h.series, h.point); + } + } + + if (item) { + highlight(item.series, item.datapoint, eventname); + } + } + + plot.getPlaceholder().trigger(eventname, [pos, item]); + } + + function highlight(s, point, auto) { + if (typeof s === "number") { + s = plot.getData()[s]; + } + + if (typeof point === "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i === -1) { + highlights.push({ + series: s, + point: point, + auto: auto + }); + + plot.triggerRedrawOverlay(); + } else if (!auto) { + highlights[i].auto = false; + } + } + + function unhighlight(s, point) { + if (s == null && point == null) { + highlights = []; + plot.triggerRedrawOverlay(); + return; + } + + if (typeof s === "number") { + s = plot.getData()[s]; + } + + if (typeof point === "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i !== -1) { + highlights.splice(i, 1); + + plot.triggerRedrawOverlay(); + } + } + + 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]) { + return i; + } + } + + return -1; + } + + function processDatapoints() { + triggerCleanupEvent(); + doTriggerClickHoverEvent(lastMouseMoveEvent, eventType.hover); + } + + function setupGrid() { + doTriggerClickHoverEvent(lastMouseMoveEvent, eventType.hover); + } + + function drawOverlay(plot, octx, overlay) { + var plotOffset = plot.getPlotOffset(), + i, hi; + + octx.save(); + octx.translate(plotOffset.left, plotOffset.top); + for (i = 0; i < highlights.length; ++i) { + hi = highlights[i]; + + if (hi.series.bars.show) drawBarHighlight(hi.series, hi.point, octx); + else drawPointHighlight(hi.series, hi.point, octx, plot); + } + octx.restore(); + } + + function drawPointHighlight(series, point, octx, plot) { + 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(); + + if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) { + return; + } + + var pointRadius = series.points.radius + series.points.lineWidth / 2; + octx.lineWidth = pointRadius; + octx.strokeStyle = highlightColor; + var radius = 1.5 * pointRadius; + x = axisx.p2c(x); + y = axisy.p2c(y); + + octx.beginPath(); + var symbol = series.points.symbol; + if (symbol === 'circle') { + octx.arc(x, y, radius, 0, 2 * Math.PI, false); + } else if (typeof symbol === 'string' && plot.drawSymbol && plot.drawSymbol[symbol]) { + plot.drawSymbol[symbol](octx, x, y, radius, false); + } + + octx.closePath(); + octx.stroke(); + } + + function drawBarHighlight(series, point, octx) { + var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), + fillStyle = highlightColor, + barLeft; + + var barWidth = series.bars.barWidth[0] || series.bars.barWidth; + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -barWidth; + break; + default: + barLeft = -barWidth / 2; + } + + octx.lineWidth = series.bars.lineWidth; + octx.strokeStyle = highlightColor; + + var fillTowards = series.bars.fillTowards || 0, + bottom = fillTowards > series.yaxis.min ? Math.min(series.yaxis.max, fillTowards) : series.yaxis.min; + + $.plot.drawSeries.drawBar(point[0], point[1], point[2] || bottom, barLeft, barLeft + barWidth, + function() { + return fillStyle; + }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); + } + + function initHover(plot, options) { + plot.highlight = highlight; + plot.unhighlight = unhighlight; + if (options.grid.hoverable || options.grid.clickable) { + plot.hooks.drawOverlay.push(drawOverlay); + plot.hooks.processDatapoints.push(processDatapoints); + plot.hooks.setupGrid.push(setupGrid); + } + + lastMouseMoveEvent = plot.getPlaceholder()[0].lastMouseMoveEvent; + } + + plot.hooks.bindEvents.push(bindEvents); + plot.hooks.shutdown.push(shutdown); + plot.hooks.processOptions.push(initHover); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'hover', + version: '0.1' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.image.js b/frontend/lib/flot/jquery.flot.image.js new file mode 100644 index 0000000..ae98fb4 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.image.js @@ -0,0 +1,249 @@ +/* Flot plugin for plotting images. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The data syntax is [ [ image, x1, y1, x2, y2 ], ... ] where (x1, y1) and +(x2, y2) are where you intend the two opposite corners of the image to end up +in the plot. Image must be a fully loaded Javascript image (you can make one +with new Image()). If the image is not complete, it's skipped when plotting. + +There are two helpers included for retrieving images. The easiest work the way +that you put in URLs instead of images in the data, like this: + + [ "myimage.png", 0, 0, 10, 10 ] + +Then call $.plot.image.loadData( data, options, callback ) where data and +options are the same as you pass in to $.plot. This loads the images, replaces +the URLs in the data with the corresponding images and calls "callback" when +all images are loaded (or failed loading). In the callback, you can then call +$.plot with the data set. See the included example. + +A more low-level helper, $.plot.image.load(urls, callback) is also included. +Given a list of URLs, it calls callback with an object mapping from URL to +Image object when all images are loaded or have failed loading. + +The plugin supports these options: + + series: { + images: { + show: boolean + anchor: "corner" or "center" + alpha: [ 0, 1 ] + } + } + +They can be specified for a specific series: + + $.plot( $("#placeholder"), [{ + data: [ ... ], + images: { ... } + ]) + +Note that because the data format is different from usual data points, you +can't use images with anything else in a specific data series. + +Setting "anchor" to "center" causes the pixels in the image to be anchored at +the corner pixel centers inside of at the pixel corners, effectively letting +half a pixel stick out to each side in the plot. + +A possible future direction could be support for tiling for large images (like +Google Maps). + +*/ + +(function ($) { + var options = { + series: { + images: { + show: false, + alpha: 1, + anchor: "corner" // or "center" + } + } + }; + + $.plot.image = {}; + + $.plot.image.loadDataImages = function (series, options, callback) { + var urls = [], points = []; + + var defaultShow = options.series.images.show; + + $.each(series, function (i, s) { + if (!(defaultShow || s.images.show)) { + return; + } + + if (s.data) { + s = s.data; + } + + $.each(s, function (i, p) { + if (typeof p[0] === "string") { + urls.push(p[0]); + points.push(p); + } + }); + }); + + $.plot.image.load(urls, function (loadedImages) { + $.each(points, function (i, p) { + var url = p[0]; + if (loadedImages[url]) { + p[0] = loadedImages[url]; + } + }); + + callback(); + }); + } + + $.plot.image.load = function (urls, callback) { + var missing = urls.length, loaded = {}; + if (missing === 0) { + callback({}); + } + + $.each(urls, function (i, url) { + var handler = function () { + --missing; + loaded[url] = this; + + if (missing === 0) { + callback(loaded); + } + }; + + $('').load(handler).error(handler).attr('src', url); + }); + }; + + function drawSeries(plot, ctx, series) { + var plotOffset = plot.getPlotOffset(); + + if (!series.images || !series.images.show) { + return; + } + + var points = series.datapoints.points, + ps = series.datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + var img = points[i], + x1 = points[i + 1], y1 = points[i + 2], + x2 = points[i + 3], y2 = points[i + 4], + xaxis = series.xaxis, yaxis = series.yaxis, + tmp; + + // actually we should check img.complete, but it + // appears to be a somewhat unreliable indicator in + // IE6 (false even after load event) + if (!img || img.width <= 0 || img.height <= 0) { + continue; + } + + if (x1 > x2) { + tmp = x2; + x2 = x1; + x1 = tmp; + } + if (y1 > y2) { + tmp = y2; + y2 = y1; + y1 = tmp; + } + + // if the anchor is at the center of the pixel, expand the + // image by 1/2 pixel in each direction + if (series.images.anchor === "center") { + tmp = 0.5 * (x2 - x1) / (img.width - 1); + x1 -= tmp; + x2 += tmp; + tmp = 0.5 * (y2 - y1) / (img.height - 1); + y1 -= tmp; + y2 += tmp; + } + + // clip + if (x1 === x2 || y1 === y2 || + x1 >= xaxis.max || x2 <= xaxis.min || + y1 >= yaxis.max || y2 <= yaxis.min) { + continue; + } + + var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height; + if (x1 < xaxis.min) { + sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1); + x1 = xaxis.min; + } + + if (x2 > xaxis.max) { + sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1); + x2 = xaxis.max; + } + + if (y1 < yaxis.min) { + sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1); + y1 = yaxis.min; + } + + if (y2 > yaxis.max) { + sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1); + y2 = yaxis.max; + } + + x1 = xaxis.p2c(x1); + x2 = xaxis.p2c(x2); + y1 = yaxis.p2c(y1); + y2 = yaxis.p2c(y2); + + // the transformation may have swapped us + if (x1 > x2) { + tmp = x2; + x2 = x1; + x1 = tmp; + } + if (y1 > y2) { + tmp = y2; + y2 = y1; + y1 = tmp; + } + + tmp = ctx.globalAlpha; + ctx.globalAlpha *= series.images.alpha; + ctx.drawImage(img, + sx1, sy1, sx2 - sx1, sy2 - sy1, + x1 + plotOffset.left, y1 + plotOffset.top, + x2 - x1, y2 - y1); + ctx.globalAlpha = tmp; + } + } + + function processRawData(plot, series, data, datapoints) { + if (!series.images.show) { + return; + } + + // format is Image, x1, y1, x2, y2 (opposite corners) + datapoints.format = [ + { required: true }, + { x: true, number: true, required: true }, + { y: true, number: true, required: true }, + { x: true, number: true, required: true }, + { y: true, number: true, required: true } + ]; + } + + function init(plot) { + plot.hooks.processRawData.push(processRawData); + plot.hooks.drawSeries.push(drawSeries); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'image', + version: '1.1' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.js b/frontend/lib/flot/jquery.flot.js new file mode 100644 index 0000000..3750dfe --- /dev/null +++ b/frontend/lib/flot/jquery.flot.js @@ -0,0 +1,2787 @@ +/* Javascript plotting library for jQuery, version 3.0.0. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ + +// the actual Flot code +(function($) { + "use strict"; + + var Canvas = window.Flot.Canvas; + + function defaultTickGenerator(axis) { + var ticks = [], + start = $.plot.saturated.saturate($.plot.saturated.floorInBase(axis.min, axis.tickSize)), + i = 0, + v = Number.NaN, + prev; + + if (start === -Number.MAX_VALUE) { + ticks.push(start); + start = $.plot.saturated.floorInBase(axis.min + axis.tickSize, axis.tickSize); + } + + do { + prev = v; + //v = start + i * axis.tickSize; + v = $.plot.saturated.multiplyAdd(axis.tickSize, i, start); + ticks.push(v); + ++i; + } while (v < axis.max && v !== prev); + + return ticks; + } + + function defaultTickFormatter(value, axis, precision) { + var oldTickDecimals = axis.tickDecimals, + expPosition = ("" + value).indexOf("e"); + + if (expPosition !== -1) { + return expRepTickFormatter(value, axis, precision); + } + + if (precision > 0) { + axis.tickDecimals = precision; + } + + var factor = axis.tickDecimals ? parseFloat('1e' + axis.tickDecimals) : 1, + 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 (axis.tickDecimals != null) { + var decimal = formatted.indexOf("."), + decimalPrecision = decimal === -1 ? 0 : formatted.length - decimal - 1; + if (decimalPrecision < axis.tickDecimals) { + var decimals = ("" + factor).substr(1, axis.tickDecimals - decimalPrecision); + formatted = (decimalPrecision ? formatted : formatted + ".") + decimals; + } + } + + axis.tickDecimals = oldTickDecimals; + return formatted; + }; + + function expRepTickFormatter(value, axis, precision) { + var expPosition = ("" + value).indexOf("e"), + exponentValue = parseInt(("" + value).substr(expPosition + 1)), + tenExponent = expPosition !== -1 ? exponentValue : (value > 0 ? Math.floor(Math.log(value) / Math.LN10) : 0), + roundWith = parseFloat('1e' + tenExponent), + x = value / roundWith; + + if (precision) { + var updatedPrecision = recomputePrecision(value, precision); + return (value / roundWith).toFixed(updatedPrecision) + 'e' + tenExponent; + } + + if (axis.tickDecimals > 0) { + return x.toFixed(recomputePrecision(value, axis.tickDecimals)) + 'e' + tenExponent; + } + return x.toFixed() + 'e' + tenExponent; + } + + function recomputePrecision(num, precision) { + //for numbers close to zero, the precision from flot will be a big number + //while for big numbers, the precision will be negative + var log10Value = Math.log(Math.abs(num)) * Math.LOG10E, + newPrecision = Math.abs(log10Value + precision); + + return newPrecision <= 20 ? Math.floor(newPrecision) : 20; + } + + /////////////////////////////////////////////////////////////////////////// + // The top-level container for the entire plot. + function Plot(placeholder, data_, options_, plugins) { + // data is on the form: + // [ series1, series2 ... ] + // where series is either just the data as [ [x1, y1], [x2, y2], ... ] + // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } + + var series = [], + options = { + // the color theme used for graphs + colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], + 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 + 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 autoScale option is on "loose" mode, + autoScale: "exact", // Available modes: "none", "loose", "exact", "sliding-window" + windowSize: null, // null or number. This is the size of sliding-window. + growOnly: null, // grow only, useful for smoother auto-scale, the scales will grow to accomodate data but won't shrink back. + 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 + showTickLabels: "major", // "none", "endpoints", "major", "all" + 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 major tick marks + showMinorTicks: null, // true = show minor tick marks, false = hide minor tick marks + showTicks: null, // true = show tick marks, false = hide all tick marks + gridLines: null, // true = show grid lines, false = hide grid lines + 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"] + offset: { below: 0, above: 0 }, // the plot drawing offset. this is calculated by the flot.navigate for each axis + boxPosition: { centerX: 0, centerY: 0 } //position of the axis on the corresponding axis box + }, + yaxis: { + autoScaleMargin: 0.02, // margin in % to add if autoScale option is on "loose" mode + autoScale: "loose", // Available modes: "none", "loose", "exact" + growOnly: null, // grow only, useful for smoother auto-scale, the scales will grow to accomodate data but won't shrink back. + position: "left", // or "right" + showTickLabels: "major", // "none", "endpoints", "major", "all" + offset: { below: 0, above: 0 }, // the plot drawing offset. this is calculated by the flot.navigate for each axis + boxPosition: { centerX: 0, centerY: 0 } //position of the axis on the corresponding axis box + }, + xaxes: [], + yaxes: [], + series: { + points: { + show: false, + radius: 3, + lineWidth: 2, // in pixels + fill: true, + fillColor: "#ffffff", + symbol: 'circle' // or callback + }, + lines: { + // we don't put in show: false so we can see + // whether lines were actively disabled + lineWidth: 1, // in pixels + fill: false, + fillColor: null, + steps: false + // Omit 'zero', so we can later default its value to + // match that of the 'fill' option. + }, + bars: { + show: false, + lineWidth: 2, // in pixels + // barWidth: number or [number, absolute] + // when 'absolute' is false, 'number' is relative to the minimum distance between points for the series + // when 'absolute' is true, 'number' is considered to be in units of the x-axis + horizontal: false, + barWidth: 0.8, + fill: true, + fillColor: null, + align: "left", // "left", "right", or "center" + zero: true + }, + shadowSize: 3, + highlightColor: null + }, + grid: { + show: true, + aboveData: false, + color: "#545454", // primary color used for outline and labels + backgroundColor: null, // null for transparent, else color + borderColor: null, // set if different from the grid color + tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" + margin: 0, // distance from the canvas edge to the grid + labelMargin: 5, // in pixels + axisMargin: 8, // in pixels + borderWidth: 1, // in pixels + minBorderMargin: null, // in pixels, null means taken from points radius + markings: null, // array of ranges or fn: axes -> array of ranges + markingsColor: "#f4f4f4", + markingsLineWidth: 2, + // interactive stuff + clickable: false, + hoverable: false, + autoHighlight: true, // highlight in case mouse is near + mouseActiveRadius: 15 // how far the mouse can be away to activate an item + }, + interaction: { + redrawOverlayInterval: 1000 / 60 // time between updates, -1 means in same flow + }, + 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: [], + setupGrid: [], + adjustSeriesDataRange: [], + setRange: [], + drawBackground: [], + drawSeries: [], + drawAxis: [], + draw: [], + axisReserveSpace: [], + bindEvents: [], + drawOverlay: [], + resize: [], + shutdown: [] + }, + plot = this; + + var eventManager = {}; + + // interactive features + + var redrawTimeout = null; + + // public functions + plot.setData = setData; + plot.setupGrid = setupGrid; + plot.draw = draw; + plot.getPlaceholder = function() { + return placeholder; + }; + plot.getCanvas = function() { + return surface.element; + }; + plot.getSurface = function() { + return surface; + }; + plot.getEventHolder = function() { + return eventHolder[0]; + }; + plot.getPlotOffset = function() { + return plotOffset; + }; + plot.width = function() { + return plotWidth; + }; + plot.height = function() { + return plotHeight; + }; + plot.offset = function() { + var o = eventHolder.offset(); + o.left += plotOffset.left; + o.top += plotOffset.top; + return o; + }; + plot.getData = function() { + return series; + }; + plot.getAxes = function() { + var res = {}; + $.each(xaxes.concat(yaxes), function(_, axis) { + if (axis) { + res[axis.direction + (axis.n !== 1 ? axis.n : "") + "axis"] = axis; + } + }); + return res; + }; + plot.getXAxes = function() { + return xaxes; + }; + plot.getYAxes = function() { + return yaxes; + }; + plot.c2p = canvasToCartesianAxisCoords; + plot.p2c = cartesianAxisToCanvasCoords; + plot.getOptions = function() { + return options; + }; + plot.triggerRedrawOverlay = triggerRedrawOverlay; + plot.pointOffset = function(point) { + return { + left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), + top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) + }; + }; + 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; + plot = null; + }; + + plot.resize = function() { + var width = placeholder.width(), + height = placeholder.height(); + surface.resize(width, height); + overlay.resize(width, height); + + executeHooks(hooks.resize, [width, height]); + }; + + plot.clearTextCache = function () { + surface.clearCache(); + overlay.clearCache(); + }; + + plot.autoScaleAxis = autoScaleAxis; + plot.computeRangeForDataSeries = computeRangeForDataSeries; + plot.adjustSeriesDataRange = adjustSeriesDataRange; + plot.findNearbyItem = findNearbyItem; + plot.findNearbyInterpolationPoint = findNearbyInterpolationPoint; + plot.computeValuePrecision = computeValuePrecision; + plot.computeTickSize = computeTickSize; + plot.addEventHandler = addEventHandler; + + // public attributes + plot.hooks = hooks; + + // initialize + var MINOR_TICKS_COUNT_CONSTANT = $.plot.uiConstants.MINOR_TICKS_COUNT_CONSTANT; + var TICK_LENGTH_CONSTANT = $.plot.uiConstants.TICK_LENGTH_CONSTANT; + initPlugins(plot); + setupCanvases(); + parseOptions(options_); + setData(data_); + setupGrid(true); + draw(); + bindEvents(); + + function executeHooks(hook, args) { + args = [plot].concat(args); + for (var i = 0; i < hook.length; ++i) { + hook[i].apply(this, args); + } + } + + function initPlugins() { + // References to key classes, allowing plugins to modify them + + var classes = { + Canvas: Canvas + }; + + for (var i = 0; i < plugins.length; ++i) { + var p = plugins[i]; + p.init(plot, classes); + if (p.options) { + $.extend(true, options, p.options); + } + } + } + + function parseOptions(opts) { + $.extend(true, options, opts); + + // $.extend merges arrays, rather than replacing them. When less + // colors are provided than the size of the default palette, we + // end up with those colors plus the remaining defaults, which is + // not expected behavior; avoid it by replacing them here. + + if (opts && 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.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 + options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; + } + + 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(); + } + + // Fill in defaults for axis options, including any unspecified + // font-spec fields, if a font-spec was provided. + + // If no x/y axis options were provided, create one of each anyway, + // 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), + variant: placeholder.css("font-variant"), + weight: placeholder.css("font-weight"), + family: placeholder.css("font-family") + }; + + axisCount = options.xaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + axisOptions = options.xaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.xaxis, axisOptions); + options.xaxes[i] = axisOptions; + + 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); + } + } + } + + axisCount = options.yaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + axisOptions = options.yaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.yaxis, axisOptions); + options.yaxes[i] = axisOptions; + + 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); + } + } + } + + // save options on axes for future reference + for (i = 0; i < options.xaxes.length; ++i) { + getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; + } + + for (i = 0; i < options.yaxes.length; ++i) { + getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; + } + + //process boxPosition options used for axis.box size + $.each(allAxes(), function(_, axis) { + axis.boxPosition = axis.options.boxPosition || {centerX: 0, centerY: 0}; + }); + + // add hooks from options + 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]); + } + + function setData(d) { + var oldseries = series; + series = parseData(d); + fillInSeriesOptions(); + processData(oldseries); + } + + function parseData(d) { + var res = []; + for (var i = 0; i < d.length; ++i) { + var s = $.extend(true, {}, options.series); + + if (d[i].data != null) { + s.data = d[i].data; // move the data instead of deep-copy + delete d[i].data; + + $.extend(true, s, d[i]); + + d[i].data = s.data; + } else { + s.data = d[i]; + } + + res.push(s); + } + + return res; + } + + function axisNumber(obj, coord) { + var a = obj[coord + "axis"]; + if (typeof a === "object") { + // if we got a real axis, extract number + a = a.n; + } + + if (typeof a !== "number") { + a = 1; // default to first axis + } + + return a; + } + + function allAxes() { + // return flat array without annoying null entries + return xaxes.concat(yaxes).filter(function(a) { + return a; + }); + } + + // canvas to axis for cartesian axes + function canvasToCartesianAxisCoords(pos) { + // return an object with x/y corresponding to all used axes + var res = {}, + i, axis; + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + 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) { + res["y" + axis.n] = axis.c2p(pos.top); + } + } + + if (res.x1 !== undefined) { + res.x = res.x1; + } + + if (res.y1 !== undefined) { + res.y = res.y1; + } + + return res; + } + + // axis to canvas for cartesian axes + function cartesianAxisToCanvasCoords(pos) { + // get canvas coords from the first pair of x/y found in pos + var res = {}, + i, axis, key; + + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) { + key = "x" + axis.n; + if (pos[key] == null && axis.n === 1) { + key = "x"; + } + + if (pos[key] != null) { + res.left = axis.p2c(pos[key]); + break; + } + } + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) { + key = "y" + axis.n; + if (pos[key] == null && axis.n === 1) { + key = "y"; + } + + if (pos[key] != null) { + res.top = axis.p2c(pos[key]); + break; + } + } + } + + return res; + } + + function getOrCreateAxis(axes, number) { + 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) + }; + } + + return axes[number - 1]; + } + + function fillInSeriesOptions() { + var neededColors = series.length, + maxIndex = -1, + i; + + // Subtract the number of series that already have fixed colors or + // color indexes from the number that we still need to generate. + + for (i = 0; i < series.length; ++i) { + var sc = series[i].color; + if (sc != null) { + neededColors--; + if (typeof sc === "number" && sc > maxIndex) { + maxIndex = sc; + } + } + } + + // If any of the series have fixed color indexes, then we need to + // generate at least as many colors as the highest index. + + if (neededColors <= maxIndex) { + neededColors = maxIndex + 1; + } + + // Generate all the colors, using first the option colors and then + // variations on those colors once they're exhausted. + + var c, colors = [], + colorPool = options.colors, + colorPoolSize = colorPool.length, + variation = 0, + definedColors = Math.max(0, series.length - neededColors); + + for (i = 0; i < neededColors; i++) { + c = $.color.parse(colorPool[(definedColors + i) % colorPoolSize] || "#666"); + + // Each time we exhaust the colors in the pool we adjust + // a scaling factor used to produce more variations on + // those colors. The factor alternates negative/positive + // to produce lighter/darker colors. + + // 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 (variation >= 0) { + if (variation < 0.5) { + variation = -variation - 0.2; + } else variation = 0; + } else variation = -variation; + } + + colors[i] = c.scale('rgb', 1 + variation); + } + + // Finalize the series options, filling in their colors + + var colori = 0, + s; + for (i = 0; i < series.length; ++i) { + s = series[i]; + + // assign colors + if (s.color == null) { + s.color = colors[colori].toString(); + ++colori; + } else if (typeof s.color === "number") { + 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) { + if (s[v] && s[v].show) { + show = false; + break; + } + } + + if (show) { + s.lines.show = true; + } + } + + // If nothing was provided for lines.zero, default it to match + // lines.fill, since areas by default should extend to zero. + + if (s.lines.zero == null) { + s.lines.zero = !!s.lines.fill; + } + + // setup axes + s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); + s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); + } + } + + function processData(prevSeries) { + var topSentry = Number.POSITIVE_INFINITY, + bottomSentry = Number.NEGATIVE_INFINITY, + i, j, k, m, + s, points, ps, val, f, p, + data, format; + + function updateAxis(axis, min, max) { + if (min < axis.datamin && min !== -Infinity) { + axis.datamin = min; + } + + if (max > axis.datamax && max !== Infinity) { + axis.datamax = max; + } + } + + function reusePoints(prevSeries, i) { + if (prevSeries && prevSeries[i] && prevSeries[i].datapoints && prevSeries[i].datapoints.points) { + return prevSeries[i].datapoints.points; + } + + return []; + } + + $.each(allAxes(), function(_, axis) { + // init axis + if (axis.options.growOnly !== true) { + axis.datamin = topSentry; + axis.datamax = bottomSentry; + } else { + if (axis.datamin === undefined) { + axis.datamin = topSentry; + } + if (axis.datamax === undefined) { + axis.datamax = bottomSentry; + } + } + axis.used = false; + }); + + for (i = 0; i < series.length; ++i) { + s = series[i]; + s.datapoints = { + points: [] + }; + + if (s.datapoints.points.length === 0) { + s.datapoints.points = reusePoints(prevSeries, i); + } + + executeHooks(hooks.processRawData, [s, s.data, s.datapoints]); + } + + // first pass: clean and copy data + for (i = 0; i < series.length; ++i) { + s = series[i]; + + data = s.data; + format = s.datapoints.format; + + if (!format) { + format = []; + // find out how to copy + format.push({ + x: true, + y: false, + number: true, + required: true, + computeRange: s.xaxis.options.autoScale !== 'none', + defaultValue: null + }); + + format.push({ + x: false, + y: true, + number: true, + required: true, + computeRange: s.yaxis.options.autoScale !== 'none', + defaultValue: null + }); + + if (s.stack || s.bars.show || (s.lines.show && s.lines.fill)) { + var expectedPs = s.datapoints.pointsize != null ? s.datapoints.pointsize : (s.data && s.data[0] && s.data[0].length ? s.data[0].length : 3); + if (expectedPs > 2) { + format.push({ + x: false, + y: true, + number: true, + required: false, + computeRange: s.yaxis.options.autoScale !== 'none', + defaultValue: 0 + }); + } + } + + s.datapoints.format = format; + } + + s.xaxis.used = s.yaxis.used = true; + + if (s.datapoints.pointsize != null) continue; // already filled in + + s.datapoints.pointsize = format.length; + ps = s.datapoints.pointsize; + points = s.datapoints.points; + + var insertSteps = s.lines.show && s.lines.steps; + + for (j = k = 0; j < data.length; ++j, k += ps) { + p = data[j]; + + var nullify = p == null; + if (!nullify) { + for (m = 0; m < ps; ++m) { + val = p[m]; + f = format[m]; + + if (f) { + if (f.number && val != null) { + val = +val; // convert to number + if (isNaN(val)) { + val = null; + } + } + + if (val == null) { + if (f.required) nullify = true; + + if (f.defaultValue != null) val = f.defaultValue; + } + } + + points[k + m] = val; + } + } + + if (nullify) { + for (m = 0; m < ps; ++m) { + val = points[k + m]; + if (val != null) { + f = format[m]; + // extract min/max info + if (f.computeRange) { + if (f.x) { + updateAxis(s.xaxis, val, val); + } + if (f.y) { + updateAxis(s.yaxis, val, val); + } + } + } + points[k + m] = null; + } + } + } + + points.length = k; //trims the internal buffer to the correct length + } + + // give the hooks a chance to run + for (i = 0; i < series.length; ++i) { + s = series[i]; + + executeHooks(hooks.processDatapoints, [s, s.datapoints]); + } + + // second pass: find datamax/datamin for auto-scaling + for (i = 0; i < series.length; ++i) { + s = series[i]; + format = s.datapoints.format; + + if (format.every(function (f) { return !f.computeRange; })) { + continue; + } + + var range = plot.adjustSeriesDataRange(s, + plot.computeRangeForDataSeries(s)); + + executeHooks(hooks.adjustSeriesDataRange, [s, range]); + + updateAxis(s.xaxis, range.xmin, range.xmax); + updateAxis(s.yaxis, range.ymin, range.ymax); + } + + $.each(allAxes(), function(_, axis) { + if (axis.datamin === topSentry) { + axis.datamin = null; + } + + if (axis.datamax === bottomSentry) { + axis.datamax = null; + } + }); + } + + function setupCanvases() { + // Make sure the placeholder is clear of everything except canvases + // 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(); + + if (placeholder.css("position") === 'static') { + placeholder.css("position", "relative"); // for positioning labels and overlay + } + + surface = new Canvas("flot-base", placeholder[0]); + overlay = new Canvas("flot-overlay", placeholder[0]); // overlay canvas for interactive features + + ctx = surface.context; + octx = overlay.context; + + // define which element we're listening for events on + eventHolder = $(overlay.element).unbind(); + + // If we're re-using a plot object, shut down the old one + + var existing = placeholder.data("plot"); + + if (existing) { + existing.shutdown(); + overlay.clear(); + } + + // save in case we get replotted + placeholder.data("plot", plot); + } + + function bindEvents() { + executeHooks(hooks.bindEvents, [eventHolder]); + } + + function addEventHandler(event, handler, eventHolder, priority) { + var key = eventHolder + event; + var eventList = eventManager[key] || []; + + eventList.push({"event": event, "handler": handler, "eventHolder": eventHolder, "priority": priority}); + eventList.sort((a, b) => b.priority - a.priority ); + eventList.forEach( eventData => { + eventData.eventHolder.unbind(eventData.event, eventData.handler); + eventData.eventHolder.bind(eventData.event, eventData.handler); + }); + + eventManager[key] = eventList; + } + + function shutdown() { + if (redrawTimeout) { + clearTimeout(redrawTimeout); + } + + executeHooks(hooks.shutdown, [eventHolder]); + } + + function setTransformationHelpers(axis) { + // set helper functions on the axis, assumes plot area + // has been computed already + + function identity(x) { + return x; + } + + var s, m, t = axis.options.transform || identity, + it = axis.options.inverseTransform; + + // precompute how much the axis is scaling a point + // in canvas space + if (axis.direction === "x") { + if (isFinite(t(axis.max) - t(axis.min))) { + s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); + } else { + s = axis.scale = 1 / Math.abs($.plot.saturated.delta(t(axis.min), t(axis.max), plotWidth)); + } + m = Math.min(t(axis.max), t(axis.min)); + } else { + if (isFinite(t(axis.max) - t(axis.min))) { + s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); + } else { + s = axis.scale = 1 / Math.abs($.plot.saturated.delta(t(axis.min), t(axis.max), plotHeight)); + } + s = -s; + m = Math.max(t(axis.max), t(axis.min)); + } + + // data point to canvas coordinate + if (t === identity) { + // slight optimization + axis.p2c = function(p) { + if (isFinite(p - m)) { + return (p - m) * s; + } else { + return (p / 4 - m / 4) * s * 4; + } + }; + } else { + axis.p2c = function(p) { + var tp = t(p); + + if (isFinite(tp - m)) { + return (tp - m) * s; + } else { + return (tp / 4 - m / 4) * s * 4; + } + }; + } + + // canvas coordinate to data point + if (!it) { + axis.c2p = function(c) { + return m + c / s; + }; + } else { + axis.c2p = function(c) { + return it(m + c / s); + }; + } + } + + function measureTickLabels(axis) { + var opts = axis.options, + ticks = opts.showTickLabels !== 'none' && axis.ticks ? axis.ticks : [], + showMajorTickLabels = opts.showTickLabels === 'major' || opts.showTickLabels === 'all', + showEndpointsTickLabels = opts.showTickLabels === 'endpoints' || opts.showTickLabels === 'all', + labelWidth = opts.labelWidth || 0, + labelHeight = opts.labelHeight || 0, + 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"; + + for (var i = 0; i < ticks.length; ++i) { + var t = ticks[i]; + var label = t.label; + + if (!t.label || + (showMajorTickLabels === false && i > 0 && i < ticks.length - 1) || + (showEndpointsTickLabels === false && (i === 0 || i === ticks.length - 1))) { + continue; + } + + if (typeof t.label === 'object') { + label = t.label.name; + } + + var info = surface.getTextInfo(layer, label, font); + + labelWidth = Math.max(labelWidth, info.width); + labelHeight = Math.max(labelHeight, info.height); + } + + axis.labelWidth = opts.labelWidth || labelWidth; + axis.labelHeight = opts.labelHeight || labelHeight; + } + + 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 + + // here reserve additional space + executeHooks(hooks.axisReserveSpace, [axis]); + + var lw = axis.labelWidth, + lh = axis.labelHeight, + pos = axis.options.position, + isXAxis = axis.direction === "x", + tickLength = axis.options.tickLength, + showTicks = axis.options.showTicks, + showMinorTicks = axis.options.showMinorTicks, + gridLines = axis.options.gridLines, + axisMargin = options.grid.axisMargin, + padding = options.grid.labelMargin, + innermost = true, + outermost = 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; + } + } + } + }); + + // The outermost axis on each side has no margin + if (outermost) { + axisMargin = 0; + } + + // Set the default tickLength if necessary + if (tickLength == null) { + tickLength = TICK_LENGTH_CONSTANT; + } + + // By default, major tick marks are visible + if (showTicks == null) { + showTicks = true; + } + + // By default, minor tick marks are visible + if (showMinorTicks == null) { + showMinorTicks = true; + } + + // By default, grid lines are visible + if (gridLines == null) { + if (innermost) { + gridLines = true; + } else { + gridLines = false; + } + } + + if (!isNaN(+tickLength)) { + padding += showTicks ? +tickLength : 0; + } + + if (isXAxis) { + lh += padding; + + 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; + } + } else { + lw += padding; + + if (pos === "left") { + axis.box = { + left: plotOffset.left + axisMargin, + width: lw + }; + plotOffset.left += lw + axisMargin; + } else { + plotOffset.right += lw + axisMargin; + axis.box = { + left: surface.width - plotOffset.right, + width: lw + }; + } + } + + // save for future reference + axis.position = pos; + axis.tickLength = tickLength; + axis.showMinorTicks = showMinorTicks; + axis.showTicks = showTicks; + axis.gridLines = gridLines; + axis.box.padding = padding; + axis.innermost = innermost; + } + + 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; + } + } + + function adjustLayoutForThingsStickingOut() { + // possibly adjust plot offset to ensure everything stays + // inside the canvas and isn't clipped off + + var minMargin = options.grid.minBorderMargin, + 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) { + minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth / 2)); + } + } + + var a, offset = {}, + margins = { + left: minMargin, + right: minMargin, + top: minMargin, + bottom: 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); + } + } + }); + + for (a in margins) { + offset[a] = margins[a] - plotOffset[a]; + } + $.each(xaxes.concat(yaxes), function(_, axis) { + alignAxisWithGrid(axis, offset, function (offset) { + return offset > 0; + }); + }); + + 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)); + } + + function alignAxisWithGrid(axis, offset, isValid) { + if (axis.direction === "x") { + if (axis.position === "bottom" && isValid(offset.bottom)) { + axis.box.top -= Math.ceil(offset.bottom); + } + if (axis.position === "top" && isValid(offset.top)) { + axis.box.top += Math.ceil(offset.top); + } + } else { + if (axis.position === "left" && isValid(offset.left)) { + axis.box.left += Math.ceil(offset.left); + } + if (axis.position === "right" && isValid(offset.right)) { + axis.box.left -= Math.ceil(offset.right); + } + } + } + + function setupGrid(autoScale) { + var i, a, axes = allAxes(), + showGrid = options.grid.show; + + // Initialize the plot's offset from the edge of the canvas + + for (a in plotOffset) { + plotOffset[a] = 0; + } + + executeHooks(hooks.processOffset, [plotOffset]); + + // If the grid is visible, add its border width to the offset + for (a in plotOffset) { + if (typeof (options.grid.borderWidth) === "object") { + plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; + } else { + plotOffset[a] += showGrid ? options.grid.borderWidth : 0; + } + } + + $.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; + setupTickFormatter(axis); + executeHooks(hooks.setRange, [axis, autoScale]); + setRange(axis, autoScale); + }); + + if (showGrid) { + plotWidth = surface.width - plotOffset.left - plotOffset.right; + plotHeight = surface.height - plotOffset.bottom - plotOffset.top; + + var allocatedAxes = $.grep(axes, function(axis) { + return axis.show || axis.reserveSpace; + }); + + $.each(allocatedAxes, function(_, axis) { + // make the ticks + setupTickGeneration(axis); + setMajorTicks(axis); + snapRangeToTicks(axis, axis.ticks, series); + + //for computing the endpoints precision, transformationHelpers are needed + setTransformationHelpers(axis); + setEndpointTicks(axis, series); + + // 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) { + allocateAxisBoxFirstPhase(allocatedAxes[i]); + } + + // make sure we've got enough space for things that + // might stick out + adjustLayoutForThingsStickingOut(); + + $.each(allocatedAxes, function(_, axis) { + allocateAxisBoxSecondPhase(axis); + }); + } + + //adjust axis and plotOffset according to grid.margins + if (options.grid.margin) { + for (a in plotOffset) { + var margin = options.grid.margin || 0; + plotOffset[a] += typeof margin === "number" ? margin : (margin[a] || 0); + } + $.each(xaxes.concat(yaxes), function(_, axis) { + alignAxisWithGrid(axis, options.grid.margin, function(offset) { + return offset !== undefined && offset !== null; + }); + }); + } + + //after adjusting the axis, plot width and height will be modified + plotWidth = surface.width - plotOffset.left - plotOffset.right; + plotHeight = surface.height - plotOffset.bottom - plotOffset.top; + + // now we got the proper plot dimensions, we can compute the scaling + $.each(axes, function(_, axis) { + setTransformationHelpers(axis); + }); + + if (showGrid) { + drawAxisLabels(); + } + + executeHooks(hooks.setupGrid, []); + } + + function widenMinMax(minimum, maximum) { + var min = (minimum === undefined ? null : minimum); + var max = (maximum === undefined ? null : maximum); + var delta = max - min; + if (delta === 0.0) { + // degenerate case + var widen = max === 0 ? 1 : 0.01; + var wmin = null; + if (min == null) { + wmin -= widen; + } + + // always widen max if we couldn't widen min to ensure we + // don't fall into min == max which doesn't work + if (max == null || min != null) { + max += widen; + } + + if (wmin != null) { + min = wmin; + } + } + + return { + min: min, + max: max + }; + } + + function autoScaleAxis(axis) { + var opts = axis.options, + min = opts.min, + max = opts.max, + datamin = axis.datamin, + datamax = axis.datamax, + delta; + + switch (opts.autoScale) { + case "none": + min = +(opts.min != null ? opts.min : datamin); + max = +(opts.max != null ? opts.max : datamax); + break; + case "loose": + if (datamin != null && datamax != null) { + min = datamin; + max = datamax; + delta = $.plot.saturated.saturate(max - min); + var margin = ((typeof opts.autoScaleMargin === 'number') ? opts.autoScaleMargin : 0.02); + min = $.plot.saturated.saturate(min - delta * margin); + max = $.plot.saturated.saturate(max + delta * margin); + + // make sure we don't go below zero if all values are positive + if (min < 0 && datamin >= 0) { + min = 0; + } + } else { + min = opts.min; + max = opts.max; + } + break; + case "exact": + min = (datamin != null ? datamin : opts.min); + max = (datamax != null ? datamax : opts.max); + break; + case "sliding-window": + if (datamax > max) { + // move the window to fit the new data, + // keeping the axis range constant + max = datamax; + min = Math.max(datamax - (opts.windowSize || 100), min); + } + break; + } + + var widenedMinMax = widenMinMax(min, max); + min = widenedMinMax.min; + max = widenedMinMax.max; + + // grow loose or grow exact supported + if (opts.growOnly === true && opts.autoScale !== "none" && opts.autoScale !== "sliding-window") { + min = (min < datamin) ? min : (datamin !== null ? datamin : min); + max = (max > datamax) ? max : (datamax !== null ? datamax : max); + } + + axis.autoScaledMin = min; + axis.autoScaledMax = max; + } + + function setRange(axis, autoScale) { + var min = typeof axis.options.min === 'number' ? axis.options.min : axis.min, + max = typeof axis.options.max === 'number' ? axis.options.max : axis.max, + plotOffset = axis.options.offset; + + if (autoScale) { + autoScaleAxis(axis); + min = axis.autoScaledMin; + max = axis.autoScaledMax; + } + + min = (min != null ? min : -1) + (plotOffset.below || 0); + max = (max != null ? max : 1) + (plotOffset.above || 0); + + if (min > max) { + var tmp = min; + min = max; + max = tmp; + axis.options.offset = { above: 0, below: 0 }; + } + + axis.min = $.plot.saturated.saturate(min); + axis.max = $.plot.saturated.saturate(max); + } + + function computeValuePrecision (min, max, direction, ticks, tickDecimals) { + var noTicks = fixupNumberOfTicks(direction, surface, ticks); + + var delta = $.plot.saturated.delta(min, max, noTicks), + dec = -Math.floor(Math.log(delta) / Math.LN10); + + //if it is called with tickDecimals, then the precision should not be greather then that + if (tickDecimals && dec > tickDecimals) { + dec = tickDecimals; + } + + var magn = parseFloat('1e' + (-dec)), + norm = delta / magn; + + if (norm > 2.25 && norm < 3 && (dec + 1) <= tickDecimals) { + //we need an extra decimals when tickSize is 2.5 + ++dec; + } + + return isFinite(dec) ? dec : 0; + }; + + function computeTickSize (min, max, noTicks, tickDecimals) { + var delta = $.plot.saturated.delta(min, max, noTicks), + dec = -Math.floor(Math.log(delta) / Math.LN10); + + //if it is called with tickDecimals, then the precision should not be greather then that + if (tickDecimals && dec > tickDecimals) { + dec = tickDecimals; + } + + var magn = parseFloat('1e' + (-dec)), + norm = delta / magn, // norm is between 1.0 and 10.0 + size; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + if (norm > 2.25 && (tickDecimals == null || (dec + 1) <= tickDecimals)) { + size = 2.5; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + return size; + } + + function getAxisTickSize(min, max, direction, options, tickDecimals) { + var noTicks; + + if (typeof options.ticks === "number" && options.ticks > 0) { + noTicks = options.ticks; + } else { + // heuristic based on the model a*sqrt(x) fitted to + // some data points that seemed reasonable + noTicks = 0.3 * Math.sqrt(direction === "x" ? surface.width : surface.height); + } + + var size = computeTickSize(min, max, noTicks, tickDecimals); + + if (options.minTickSize != null && size < options.minTickSize) { + size = options.minTickSize; + } + + return options.tickSize || size; + }; + + function fixupNumberOfTicks(direction, surface, ticksOption) { + var noTicks; + + if (typeof ticksOption === "number" && ticksOption > 0) { + noTicks = ticksOption; + } else { + noTicks = 0.3 * Math.sqrt(direction === "x" ? surface.width : surface.height); + } + + return noTicks; + } + + function setupTickFormatter(axis) { + var opts = axis.options; + if (!axis.tickFormatter) { + if (typeof opts.tickFormatter === 'function') { + axis.tickFormatter = function() { + var args = Array.prototype.slice.call(arguments); + return "" + opts.tickFormatter.apply(null, args); + }; + } else { + axis.tickFormatter = defaultTickFormatter; + } + } + } + + function setupTickGeneration(axis) { + var opts = axis.options; + var noTicks; + + noTicks = fixupNumberOfTicks(axis.direction, surface, opts.ticks); + + axis.delta = $.plot.saturated.delta(axis.min, axis.max, noTicks); + var precision = plot.computeValuePrecision(axis.min, axis.max, axis.direction, noTicks, opts.tickDecimals); + + axis.tickDecimals = Math.max(0, opts.tickDecimals != null ? opts.tickDecimals : precision); + axis.tickSize = getAxisTickSize(axis.min, axis.max, axis.direction, opts, opts.tickDecimals); + + // Flot supports base-10 axes; any other mode else is handled by a plug-in, + // like flot.time.js. + + if (!axis.tickGenerator) { + if (typeof opts.tickGenerator === 'function') { + axis.tickGenerator = opts.tickGenerator; + } else { + axis.tickGenerator = defaultTickGenerator; + } + } + + if (opts.alignTicksWithAxis != null) { + 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, plot); + if (niceTicks.length > 0) { + if (opts.min == null) { + axis.min = Math.min(axis.min, niceTicks[0]); + } + + if (opts.max == null && niceTicks.length > 1) { + axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); + } + } + + axis.tickGenerator = function(axis) { + // copy ticks, scaled to this axis + var ticks = [], + v, i; + for (i = 0; i < otherAxis.ticks.length; ++i) { + v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); + v = axis.min + v * (axis.max - axis.min); + ticks.push(v); + } + return ticks; + }; + + // we might need an extra decimal since forced + // ticks don't necessarily fit naturally + if (!axis.mode && opts.tickDecimals == null) { + var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), + ts = axis.tickGenerator(axis, plot); + + // 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)))) { + axis.tickDecimals = extraDec; + } + } + } + } + } + + function setMajorTicks(axis) { + var oticks = axis.options.ticks, + ticks = []; + if (oticks == null || (typeof oticks === "number" && oticks > 0)) { + ticks = axis.tickGenerator(axis, plot); + } else if (oticks) { + if ($.isFunction(oticks)) { + // generate the ticks + ticks = oticks(axis); + } else { + ticks = oticks; + } + } + + // clean up/labelify the supplied ticks, copy them over + var i, v; + axis.ticks = []; + for (i = 0; i < ticks.length; ++i) { + var label = null; + var t = ticks[i]; + if (typeof t === "object") { + v = +t[0]; + if (t.length > 1) { + label = t[1]; + } + } else { + v = +t; + } + + if (!isNaN(v)) { + axis.ticks.push( + newTick(v, label, axis, 'major')); + } + } + } + + function newTick(v, label, axis, type) { + if (label === null) { + switch (type) { + case 'min': + case 'max': + //improving the precision of endpoints + var precision = getEndpointPrecision(v, axis); + label = isFinite(precision) ? axis.tickFormatter(v, axis, precision, plot) : axis.tickFormatter(v, axis, precision, plot); + break; + case 'major': + label = axis.tickFormatter(v, axis, undefined, plot); + } + } + return { + v: v, + label: label + }; + } + + function snapRangeToTicks(axis, ticks, series) { + var anyDataInSeries = function(series) { + return series.some(e => e.datapoints.points.length > 0); + } + + if (axis.options.autoScale === "loose" && ticks.length > 0 && anyDataInSeries(series)) { + // snap to ticks + axis.min = Math.min(axis.min, ticks[0].v); + axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); + } + } + + function getEndpointPrecision(value, axis) { + var canvas1 = Math.floor(axis.p2c(value)), + canvas2 = axis.direction === "x" ? canvas1 + 1 : canvas1 - 1, + point1 = axis.c2p(canvas1), + point2 = axis.c2p(canvas2), + precision = computeValuePrecision(point1, point2, axis.direction, 1); + + return precision; + } + + function setEndpointTicks(axis, series) { + if (isValidEndpointTick(axis, series)) { + axis.ticks.unshift(newTick(axis.min, null, axis, 'min')); + axis.ticks.push(newTick(axis.max, null, axis, 'max')); + } + } + + function isValidEndpointTick(axis, series) { + if (axis.options.showTickLabels === 'endpoints') { + return true; + } + if (axis.options.showTickLabels === 'all') { + var associatedSeries = series.filter(function(s) { + return s.xaxis === axis; + }), + notAllBarSeries = associatedSeries.some(function(s) { + return !s.bars.show; + }); + return associatedSeries.length === 0 || notAllBarSeries; + } + if (axis.options.showTickLabels === 'major' || axis.options.showTickLabels === 'none') { + return false; + } + } + + function draw() { + surface.clear(); + executeHooks(hooks.drawBackground, [ctx]); + + var grid = options.grid; + + // draw background, if any + if (grid.show && grid.backgroundColor) { + drawBackground(); + } + + if (grid.show && !grid.aboveData) { + drawGrid(); + } + + for (var i = 0; i < series.length; ++i) { + executeHooks(hooks.drawSeries, [ctx, series[i], i, getColorOrGradient]); + drawSeries(series[i]); + } + + executeHooks(hooks.draw, [ctx]); + + if (grid.show && grid.aboveData) { + drawGrid(); + } + + surface.render(); + + // A draw implies that either the axes or data have changed, so we + // should probably update the overlay highlights as well. + triggerRedrawOverlay(); + } + + function extractRange(ranges, coord) { + var axis, from, to, key, axes = allAxes(); + + for (var i = 0; i < axes.length; ++i) { + axis = axes[i]; + if (axis.direction === coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n === 1) { + // support x1axis as xaxis + key = coord + "axis"; + } + + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } + } + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord === "x" ? xaxes[0] : yaxes[0]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } + + return { + from: from, + to: to, + axis: axis + }; + } + + function drawBackground() { + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); + ctx.fillRect(0, 0, plotWidth, plotHeight); + ctx.restore(); + } + + function drawMarkings() { + // draw markings + var markings = options.grid.markings, + axes; + + if (markings) { + if ($.isFunction(markings)) { + axes = plot.getAxes(); + // xmin etc. is backwards compatibility, to be + // removed in the future + axes.xmin = axes.xaxis.min; + axes.xmax = axes.xaxis.max; + axes.ymin = axes.yaxis.min; + axes.ymax = axes.yaxis.max; + + markings = markings(axes); + } + + var i; + for (i = 0; i < markings.length; ++i) { + var m = markings[i], + xrange = extractRange(m, "x"), + yrange = extractRange(m, "y"); + + // fill in missing + if (xrange.from == null) { + xrange.from = xrange.axis.min; + } + + if (xrange.to == null) { + xrange.to = xrange.axis.max; + } + + if (yrange.from == null) { + yrange.from = yrange.axis.min; + } + + 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) { + 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) { + 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; + 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.stroke(); + } else { + ctx.fillStyle = m.color || options.grid.markingsColor; + ctx.fillRect(xrange.from, yrange.to, + xrange.to - xrange.from, + yrange.from - yrange.to); + } + } + } + } + + function findEdges(axis) { + var box = axis.box, + x = 0, + y = 0; + + // find the edges + if (axis.direction === "x") { + x = 0; + y = box.top - plotOffset.top + (axis.position === "top" ? box.height : 0); + } else { + y = 0; + x = box.left - plotOffset.left + (axis.position === "left" ? box.width : 0) + axis.boxPosition.centerX; + } + + return { + x: x, + y: y + }; + }; + + function alignPosition(lineWidth, pos) { + return ((lineWidth % 2) !== 0) ? Math.floor(pos) + 0.5 : pos; + }; + + function drawTickBar(axis) { + ctx.lineWidth = 1; + var edges = findEdges(axis), + x = edges.x, + y = edges.y; + + // draw tick bar + if (axis.show) { + var xoff = 0, + yoff = 0; + + ctx.strokeStyle = axis.options.color; + ctx.beginPath(); + if (axis.direction === "x") { + xoff = plotWidth + 1; + } else { + yoff = plotHeight + 1; + } + + if (axis.direction === "x") { + y = alignPosition(ctx.lineWidth, y); + } else { + x = alignPosition(ctx.lineWidth, x); + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + ctx.stroke(); + } + }; + + function drawTickMarks(axis) { + var t = axis.tickLength, + minorTicks = axis.showMinorTicks, + minorTicksNr = MINOR_TICKS_COUNT_CONSTANT, + edges = findEdges(axis), + x = edges.x, + y = edges.y, + i = 0; + + // draw major tick marks + ctx.strokeStyle = axis.options.color; + ctx.beginPath(); + + for (i = 0; i < axis.ticks.length; ++i) { + var v = axis.ticks[i].v, + xoff = 0, + yoff = 0, + xminor = 0, + yminor = 0, + j; + + if (!isNaN(v) && v >= axis.min && v <= axis.max) { + if (axis.direction === "x") { + x = axis.p2c(v); + yoff = t; + + if (axis.position === "top") { + yoff = -yoff; + } + } else { + y = axis.p2c(v); + xoff = t; + + if (axis.position === "left") { + xoff = -xoff; + } + } + + if (axis.direction === "x") { + x = alignPosition(ctx.lineWidth, x); + } else { + y = alignPosition(ctx.lineWidth, y); + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + } + + //draw minor tick marks + if (minorTicks === true && i < axis.ticks.length - 1) { + var v1 = axis.ticks[i].v, + v2 = axis.ticks[i + 1].v, + step = (v2 - v1) / (minorTicksNr + 1); + + for (j = 1; j <= minorTicksNr; j++) { + // compute minor tick position + if (axis.direction === "x") { + yminor = t / 2; // minor ticks are half length + x = alignPosition(ctx.lineWidth, axis.p2c(v1 + j * step)) + + if (axis.position === "top") { + yminor = -yminor; + } + + // don't go over the plot borders + if ((x < 0) || (x > plotWidth)) { + continue; + } + } else { + xminor = t / 2; // minor ticks are half length + y = alignPosition(ctx.lineWidth, axis.p2c(v1 + j * step)); + + if (axis.position === "left") { + xminor = -xminor; + } + + // don't go over the plot borders + if ((y < 0) || (y > plotHeight)) { + continue; + } + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xminor, y + yminor); + } + } + } + + ctx.stroke(); + }; + + function drawGridLines(axis) { + // check if the line will be overlapped with a border + var overlappedWithBorder = function (value) { + var bw = options.grid.borderWidth; + return (((typeof bw === "object" && bw[axis.position] > 0) || bw > 0) && (value === axis.min || value === axis.max)); + }; + + ctx.strokeStyle = options.grid.tickColor; + ctx.beginPath(); + var i; + for (i = 0; i < axis.ticks.length; ++i) { + var v = axis.ticks[i].v, + xoff = 0, + yoff = 0, + x = 0, + y = 0; + + if (isNaN(v) || v < axis.min || v > axis.max) continue; + + // skip those lying on the axes if we got a border + if (overlappedWithBorder(v)) continue; + + if (axis.direction === "x") { + x = axis.p2c(v); + y = plotHeight; + yoff = -plotHeight; + } else { + x = 0; + y = axis.p2c(v); + xoff = plotWidth; + } + + if (axis.direction === "x") { + x = alignPosition(ctx.lineWidth, x); + } else { + y = alignPosition(ctx.lineWidth, y); + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + } + + ctx.stroke(); + }; + + function drawBorder() { + // If either borderWidth or borderColor is an object, then draw the border + // line by line instead of as one rectangle + var bw = options.grid.borderWidth, + bc = options.grid.borderColor; + + if (typeof bw === "object" || typeof bc === "object") { + if (typeof bw !== "object") { + bw = { + top: bw, + right: bw, + bottom: bw, + left: bw + }; + } + if (typeof bc !== "object") { + bc = { + top: bc, + right: bc, + bottom: bc, + left: bc + }; + } + + if (bw.top > 0) { + ctx.strokeStyle = bc.top; + ctx.lineWidth = bw.top; + ctx.beginPath(); + ctx.moveTo(0 - bw.left, 0 - bw.top / 2); + ctx.lineTo(plotWidth, 0 - bw.top / 2); + ctx.stroke(); + } + + if (bw.right > 0) { + ctx.strokeStyle = bc.right; + ctx.lineWidth = bw.right; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); + ctx.lineTo(plotWidth + bw.right / 2, plotHeight); + ctx.stroke(); + } + + if (bw.bottom > 0) { + ctx.strokeStyle = bc.bottom; + ctx.lineWidth = bw.bottom; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); + ctx.lineTo(0, plotHeight + bw.bottom / 2); + ctx.stroke(); + } + + if (bw.left > 0) { + ctx.strokeStyle = bc.left; + ctx.lineWidth = bw.left; + ctx.beginPath(); + ctx.moveTo(0 - bw.left / 2, plotHeight + bw.bottom); + ctx.lineTo(0 - bw.left / 2, 0); + ctx.stroke(); + } + } else { + ctx.lineWidth = bw; + ctx.strokeStyle = options.grid.borderColor; + ctx.strokeRect(-bw / 2, -bw / 2, plotWidth + bw, plotHeight + bw); + } + }; + + function drawGrid() { + var axes, bw; + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + drawMarkings(); + + axes = allAxes(); + bw = options.grid.borderWidth; + + for (var j = 0; j < axes.length; ++j) { + var axis = axes[j]; + + if (!axis.show) { + continue; + } + + drawTickBar(axis); + if (axis.showTicks === true) { + drawTickMarks(axis); + } + + if (axis.gridLines === true) { + drawGridLines(axis, bw); + } + } + + // draw border + if (bw) { + drawBorder(); + } + + ctx.restore(); + } + + function drawAxisLabels() { + $.each(allAxes(), function(_, axis) { + 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", + i, x, y, halign, valign, info, + margin = 3, + nullBox = {x: NaN, y: NaN, width: NaN, height: NaN}, newLabelBox, labelBoxes = [], + overlapping = function(x11, y11, x12, y12, x21, y21, x22, y22) { + return ((x11 <= x21 && x21 <= x12) || (x21 <= x11 && x11 <= x22)) && + ((y11 <= y21 && y21 <= y12) || (y21 <= y11 && y11 <= y22)); + }, + overlapsOtherLabels = function(newLabelBox, previousLabelBoxes) { + return previousLabelBoxes.some(function(labelBox) { + return overlapping( + newLabelBox.x, newLabelBox.y, newLabelBox.x + newLabelBox.width, newLabelBox.y + newLabelBox.height, + labelBox.x, labelBox.y, labelBox.x + labelBox.width, labelBox.y + labelBox.height); + }); + }, + drawAxisLabel = function (tick, labelBoxes) { + if (!tick || !tick.label || tick.v < axis.min || tick.v > axis.max) { + return nullBox; + } + + info = surface.getTextInfo(layer, tick.label, font); + + if (axis.direction === "x") { + halign = "center"; + x = plotOffset.left + axis.p2c(tick.v); + if (axis.position === "bottom") { + y = box.top + box.padding - axis.boxPosition.centerY; + } else { + y = box.top + box.height - box.padding + axis.boxPosition.centerY; + valign = "bottom"; + } + newLabelBox = {x: x - info.width / 2 - margin, y: y - margin, width: info.width + 2 * margin, height: info.height + 2 * margin}; + } else { + valign = "middle"; + y = plotOffset.top + axis.p2c(tick.v); + if (axis.position === "left") { + x = box.left + box.width - box.padding - axis.boxPosition.centerX; + halign = "right"; + } else { + x = box.left + box.padding + axis.boxPosition.centerX; + } + newLabelBox = {x: x - info.width / 2 - margin, y: y - margin, width: info.width + 2 * margin, height: info.height + 2 * margin}; + } + + if (overlapsOtherLabels(newLabelBox, labelBoxes)) { + return nullBox; + } + + surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); + + return newLabelBox; + }; + + // 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); + + executeHooks(hooks.drawAxis, [axis, surface]); + + if (!axis.show) { + return; + } + + switch (axis.options.showTickLabels) { + case 'none': + break; + case 'endpoints': + labelBoxes.push(drawAxisLabel(axis.ticks[0], labelBoxes)); + labelBoxes.push(drawAxisLabel(axis.ticks[axis.ticks.length - 1], labelBoxes)); + break; + case 'major': + labelBoxes.push(drawAxisLabel(axis.ticks[0], labelBoxes)); + labelBoxes.push(drawAxisLabel(axis.ticks[axis.ticks.length - 1], labelBoxes)); + for (i = 1; i < axis.ticks.length - 1; ++i) { + labelBoxes.push(drawAxisLabel(axis.ticks[i], labelBoxes)); + } + break; + case 'all': + labelBoxes.push(drawAxisLabel(axis.ticks[0], [])); + labelBoxes.push(drawAxisLabel(axis.ticks[axis.ticks.length - 1], labelBoxes)); + for (i = 1; i < axis.ticks.length - 1; ++i) { + labelBoxes.push(drawAxisLabel(axis.ticks[i], labelBoxes)); + } + break; + } + }); + } + + function drawSeries(series) { + if (series.lines.show) { + $.plot.drawSeries.drawSeriesLines(series, ctx, plotOffset, plotWidth, plotHeight, plot.drawSymbol, getColorOrGradient); + } + + if (series.bars.show) { + $.plot.drawSeries.drawSeriesBars(series, ctx, plotOffset, plotWidth, plotHeight, plot.drawSymbol, getColorOrGradient); + } + + if (series.points.show) { + $.plot.drawSeries.drawSeriesPoints(series, ctx, plotOffset, plotWidth, plotHeight, plot.drawSymbol, getColorOrGradient); + } + } + + function computeRangeForDataSeries(series, force, isValid) { + var points = series.datapoints.points, + ps = series.datapoints.pointsize, + format = series.datapoints.format, + topSentry = Number.POSITIVE_INFINITY, + bottomSentry = Number.NEGATIVE_INFINITY, + range = { + xmin: topSentry, + ymin: topSentry, + xmax: bottomSentry, + ymax: bottomSentry + }; + + for (var j = 0; j < points.length; j += ps) { + if (points[j] === null) { + continue; + } + + if (typeof (isValid) === 'function' && !isValid(points[j])) { + continue; + } + + for (var m = 0; m < ps; ++m) { + var val = points[j + m], + f = format[m]; + if (f === null || f === undefined) { + continue; + } + + if (typeof (isValid) === 'function' && !isValid(val)) { + continue; + } + + if ((!force && !f.computeRange) || val === Infinity || val === -Infinity) { + continue; + } + + if (f.x === true) { + if (val < range.xmin) { + range.xmin = val; + } + + if (val > range.xmax) { + range.xmax = val; + } + } + + if (f.y === true) { + if (val < range.ymin) { + range.ymin = val; + } + + if (val > range.ymax) { + range.ymax = val; + } + } + } + } + + return range; + }; + + function adjustSeriesDataRange(series, range) { + if (series.bars.show) { + // make sure we got room for the bar on the dancing floor + var delta; + + // update bar width if needed + var useAbsoluteBarWidth = series.bars.barWidth[1]; + if (series.datapoints && series.datapoints.points && !useAbsoluteBarWidth) { + computeBarWidth(series); + } + + var barWidth = series.bars.barWidth[0] || series.bars.barWidth; + switch (series.bars.align) { + case "left": + delta = 0; + break; + case "right": + delta = -barWidth; + break; + default: + delta = -barWidth / 2; + } + + if (series.bars.horizontal) { + range.ymin += delta; + range.ymax += delta + barWidth; + } + else { + range.xmin += delta; + range.xmax += delta + barWidth; + } + } + + if ((series.bars.show && series.bars.zero) || (series.lines.show && series.lines.zero)) { + var ps = series.datapoints.pointsize; + + // make sure the 0 point is included in the computed y range when requested + if (ps <= 2) { + /*if ps > 0 the points were already taken into account for autoScale */ + range.ymin = Math.min(0, range.ymin); + range.ymax = Math.max(0, range.ymax); + } + } + + return range; + }; + + function computeBarWidth(series) { + var xValues = []; + var pointsize = series.datapoints.pointsize, minDistance = Number.MAX_VALUE; + + if (series.datapoints.points.length <= pointsize) { + minDistance = 1; + } + + var start = series.bars.horizontal ? 1 : 0; + for (var j = start; j < series.datapoints.points.length; j += pointsize) { + if (isFinite(series.datapoints.points[j]) && series.datapoints.points[j] !== null) { + xValues.push(series.datapoints.points[j]); + } + } + + function onlyUnique(value, index, self) { + return self.indexOf(value) === index; + } + + xValues = xValues.filter( onlyUnique ); + xValues.sort(function(a, b){return a - b}); + + for (var j = 1; j < xValues.length; j++) { + var distance = Math.abs(xValues[j] - xValues[j - 1]); + if (distance < minDistance && isFinite(distance)) { + minDistance = distance; + } + } + + if (typeof series.bars.barWidth === "number") { + series.bars.barWidth = series.bars.barWidth * minDistance; + } else { + series.bars.barWidth[0] = series.bars.barWidth[0] * minDistance; + } + } + + // returns the data item the mouse is over/ the cursor is closest to, or null if none is found + function findNearbyItem(mouseX, mouseY, seriesFilter, radius, computeDistance) { + var i, j, + item = null, + smallestDistance = radius * radius + 1; + + for (var i = series.length - 1; i >= 0; --i) { + if (!seriesFilter(i)) continue; + + var s = series[i]; + if (!s.datapoints) return; + + if (s.lines.show || s.points.show) { + var found = findNearbyPoint(s, mouseX, mouseY, radius, smallestDistance, computeDistance); + if (found) { + smallestDistance = found.distance; + item = [i, found.dataIndex]; + } + } + + if (s.bars.show && !item) { // no other point can be nearby + var foundIndex = findNearbyBar(s, mouseX, mouseY); + if (foundIndex >= 0) item = [i, foundIndex]; + } + } + + if (item) { + i = item[0]; + j = item[1]; + var ps = series[i].datapoints.pointsize; + + return { + datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), + dataIndex: j, + series: series[i], + seriesIndex: i + }; + } + + return null; + } + + function findNearbyPoint (series, mouseX, mouseY, maxDistance, smallestDistance, computeDistance) { + var mx = series.xaxis.c2p(mouseX), + my = series.yaxis.c2p(mouseY), + maxx = maxDistance / series.xaxis.scale, + maxy = maxDistance / series.yaxis.scale, + points = series.datapoints.points, + ps = series.datapoints.pointsize; + + // with inverse transforms, we can't use the maxx/maxy + // optimization, sadly + if (series.xaxis.options.inverseTransform) { + maxx = Number.MAX_VALUE; + } + + if (series.yaxis.options.inverseTransform) { + maxy = Number.MAX_VALUE; + } + + var found = null; + for (var j = 0; j < points.length; j += ps) { + var x = points[j]; + var y = points[j + 1]; + if (x == null) { + continue; + } + + if (x - mx > maxx || x - mx < -maxx || + 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 + var dx = Math.abs(series.xaxis.p2c(x) - mouseX); + var dy = Math.abs(series.yaxis.p2c(y) - mouseY); + var dist = computeDistance ? computeDistance(dx, dy) : dx * dx + dy * dy; + + // use <= to ensure last point takes precedence + // (last generally means on top of) + if (dist < smallestDistance) { + smallestDistance = dist; + found = { dataIndex: j / ps, distance: dist }; + } + } + + return found; + } + + function findNearbyBar (series, mouseX, mouseY) { + var barLeft, barRight, + barWidth = series.bars.barWidth[0] || series.bars.barWidth, + mx = series.xaxis.c2p(mouseX), + my = series.yaxis.c2p(mouseY), + points = series.datapoints.points, + ps = series.datapoints.pointsize; + + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -barWidth; + break; + default: + barLeft = -barWidth / 2; + } + + barRight = barLeft + barWidth; + + var fillTowards = series.bars.fillTowards || 0; + var defaultBottom = fillTowards > series.yaxis.min ? Math.min(series.yaxis.max, fillTowards) : series.yaxis.min; + + var foundIndex = -1; + for (var j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1]; + if (x == null) + continue; + + var bottom = ps === 3 ? points[j + 2] : defaultBottom; + // for a bar graph, the cursor must be inside the bar + if (series.bars.horizontal ? + (mx <= Math.max(bottom, x) && mx >= Math.min(bottom, x) && + my >= y + barLeft && my <= y + barRight) : + (mx >= x + barLeft && mx <= x + barRight && + my >= Math.min(bottom, y) && my <= Math.max(bottom, y))) + foundIndex = j / ps; + } + + return foundIndex; + } + + function findNearbyInterpolationPoint(posX, posY, seriesFilter) { + var i, j, dist, dx, dy, ps, + item, + smallestDistance = Number.MAX_VALUE; + + for (i = 0; i < series.length; ++i) { + if (!seriesFilter(i)) { + continue; + } + var points = series[i].datapoints.points; + ps = series[i].datapoints.pointsize; + + // if the data is coming from positive -> negative, reverse the comparison + const comparer = points[points.length - ps] < points[0] + ? function (x1, x2) { return x1 > x2 } + : function (x1, x2) { return x2 > x1 }; + + // do not interpolate outside the bounds of the data. + if (comparer(posX, points[0])) { + continue; + } + + // Find the nearest points, x-wise + for (j = ps; j < points.length; j += ps) { + if (comparer(posX, points[j])) { + break; + } + } + + // Now Interpolate + var y, + p1x = points[j - ps], + p1y = points[j - ps + 1], + p2x = points[j], + p2y = points[j + 1]; + + if ((p1x === undefined) || (p2x === undefined) || + (p1y === undefined) || (p2y === undefined)) { + continue; + } + + if (p1x === p2x) { + y = p2y + } else { + y = p1y + (p2y - p1y) * (posX - p1x) / (p2x - p1x); + } + + posY = y; + + dx = Math.abs(series[i].xaxis.p2c(p2x) - posX); + dy = Math.abs(series[i].yaxis.p2c(p2y) - posY); + dist = dx * dx + dy * dy; + + if (dist < smallestDistance) { + smallestDistance = dist; + item = [posX, posY, i, j]; + } + } + + if (item) { + i = item[2]; + j = item[3]; + ps = series[i].datapoints.pointsize; + points = series[i].datapoints.points; + p1x = points[j - ps]; + p1y = points[j - ps + 1]; + p2x = points[j]; + p2y = points[j + 1]; + + return { + datapoint: [item[0], item[1]], + leftPoint: [p1x, p1y], + rightPoint: [p2x, p2y], + seriesIndex: i + }; + } + + return null; + } + + function triggerRedrawOverlay() { + var t = options.interaction.redrawOverlayInterval; + if (t === -1) { // skip event queue + drawOverlay(); + return; + } + + if (!redrawTimeout) { + redrawTimeout = setTimeout(function() { + drawOverlay(plot); + }, t); + } + } + + function drawOverlay(plot) { + redrawTimeout = null; + + if (!octx) { + return; + } + overlay.clear(); + executeHooks(hooks.drawOverlay, [octx, overlay]); + var event = new CustomEvent('onDrawingDone'); + plot.getEventHolder().dispatchEvent(event); + plot.getPlaceholder().trigger('drawingdone'); + } + + function getColorOrGradient(spec, bottom, top, defaultColor) { + if (typeof spec === "string") { + return spec; + } else { + // assume this is a gradient spec; IE currently only + // supports a simple vertical gradient properly, so that's + // what we support too + var gradient = ctx.createLinearGradient(0, top, 0, bottom); + + for (var i = 0, l = spec.colors.length; i < l; ++i) { + var c = spec.colors[i]; + if (typeof c !== "string") { + var co = $.color.parse(defaultColor); + 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); + } + + return gradient; + } + } + } + + // Add the plot function to the top level of the jQuery object + + $.plot = function(placeholder, data, options) { + var plot = new Plot($(placeholder), data, options, $.plot.plugins); + return plot; + }; + + $.plot.version = "3.0.0"; + + $.plot.plugins = []; + + // Also add the plot function as a chainable property + $.fn.plot = function(data, options) { + return this.each(function() { + $.plot(this, data, options); + }); + }; + + $.plot.linearTickGenerator = defaultTickGenerator; + $.plot.defaultTickFormatter = defaultTickFormatter; + $.plot.expRepTickFormatter = expRepTickFormatter; +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.legend.js b/frontend/lib/flot/jquery.flot.legend.js new file mode 100644 index 0000000..eb48ac9 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.legend.js @@ -0,0 +1,437 @@ +/* Flot plugin for drawing legends. + +*/ + +(function($) { + var defaultOptions = { + legend: { + show: false, + noColumns: 1, + labelFormatter: null, // fn: string -> string + container: null, // container (as jQuery object) to put legend in, null means default on top of graph + position: 'ne', // position of default legend container within plot + margin: 5, // distance from grid edge to default legend container within plot + sorted: null // default to no legend sorting + } + }; + + function insertLegend(plot, options, placeholder, legendEntries) { + // clear before redraw + if (options.legend.container != null) { + $(options.legend.container).html(''); + } else { + placeholder.find('.legend').remove(); + } + + if (!options.legend.show) { + return; + } + + // Save the legend entries in legend options + var entries = options.legend.legendEntries = legendEntries, + plotOffset = options.legend.plotOffset = plot.getPlotOffset(), + html = [], + entry, labelHtml, iconHtml, + j = 0, + i, + pos = "", + p = options.legend.position, + m = options.legend.margin, + shape = { + name: '', + label: '', + xPos: '', + yPos: '' + }; + + html[j++] = ''; + html[j++] = ''; + html[j++] = svgShapeDefs; + + var left = 0; + var columnWidths = []; + var style = window.getComputedStyle(document.querySelector('body')); + for (i = 0; i < entries.length; ++i) { + var columnIndex = i % options.legend.noColumns; + entry = entries[i]; + shape.label = entry.label; + var info = plot.getSurface().getTextInfo('', shape.label, { + style: style.fontStyle, + variant: style.fontVariant, + weight: style.fontWeight, + size: parseInt(style.fontSize), + lineHeight: parseInt(style.lineHeight), + family: style.fontFamily + }); + + var labelWidth = info.width; + // 36px = 1.5em + 6px margin + var iconWidth = 48; + if (columnWidths[columnIndex]) { + if (labelWidth > columnWidths[columnIndex]) { + columnWidths[columnIndex] = labelWidth + iconWidth; + } + } else { + columnWidths[columnIndex] = labelWidth + iconWidth; + } + } + + // Generate html for icons and labels from a list of entries + for (i = 0; i < entries.length; ++i) { + var columnIndex = i % options.legend.noColumns; + entry = entries[i]; + iconHtml = ''; + shape.label = entry.label; + shape.xPos = (left + 3) + 'px'; + left += columnWidths[columnIndex]; + if ((i + 1) % options.legend.noColumns === 0) { + left = 0; + } + shape.yPos = Math.floor(i / options.legend.noColumns) * 1.5 + 'em'; + // area + if (entry.options.lines.show && entry.options.lines.fill) { + shape.name = 'area'; + shape.fillColor = entry.color; + iconHtml += getEntryIconHtml(shape); + } + // bars + if (entry.options.bars.show) { + shape.name = 'bar'; + shape.fillColor = entry.color; + iconHtml += getEntryIconHtml(shape); + } + // lines + if (entry.options.lines.show && !entry.options.lines.fill) { + shape.name = 'line'; + shape.strokeColor = entry.color; + shape.strokeWidth = entry.options.lines.lineWidth; + iconHtml += getEntryIconHtml(shape); + } + // points + if (entry.options.points.show) { + shape.name = entry.options.points.symbol; + shape.strokeColor = entry.color; + shape.fillColor = entry.options.points.fillColor; + shape.strokeWidth = entry.options.points.lineWidth; + iconHtml += getEntryIconHtml(shape); + } + + labelHtml = '' + shape.label + '' + html[j++] = '' + iconHtml + labelHtml + ''; + } + + html[j++] = ''; + 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 width = 6; + for (i = 0; i < columnWidths.length; ++i) { + width += columnWidths[i]; + } + + var legendEl, + height = Math.ceil(entries.length / options.legend.noColumns) * 1.6; + if (!options.legend.container) { + legendEl = $('
' + html.join('') + '
').appendTo(placeholder); + legendEl.css('width', width + 'px'); + legendEl.css('height', height + 'em'); + legendEl.css('pointerEvents', 'none'); + } else { + legendEl = $(html.join('')).appendTo(options.legend.container)[0]; + options.legend.container.style.width = width + 'px'; + options.legend.container.style.height = height + 'em'; + } + } + + // Generate html for a shape + function getEntryIconHtml(shape) { + var html = '', + name = shape.name, + x = shape.xPos, + y = shape.yPos, + fill = shape.fillColor, + stroke = shape.strokeColor, + width = shape.strokeWidth; + switch (name) { + case 'circle': + html = ''; + break; + case 'diamond': + html = ''; + break; + case 'cross': + html = ''; + break; + case 'rectangle': + html = ''; + break; + case 'plus': + html = ''; + break; + case 'bar': + html = ''; + break; + case 'area': + html = ''; + break; + case 'line': + html = ''; + break; + default: + // default is circle + html = ''; + } + + return html; + } + + // Define svg symbols for shapes + var svgShapeDefs = '' + + '' + + '' + + '' + + '' + + + '' + + '' + + '' + + + '' + + '' + + '' + + + '' + + '' + + '' + + '' + + '' + + '' + + + '' + + '' + + '' + + '' + + '' + + '' + + + '' + + '' + + '' + + '' + + '' + + '' + + + '' + + '' + + '' + + '' + + '' + + '' + + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + + // Generate a list of legend entries in their final order + function getLegendEntries(series, labelFormatter, sorted) { + var lf = labelFormatter, + legendEntries = series.reduce(function(validEntries, s, i) { + var labelEval = (lf ? lf(s.label, s) : s.label) + if (s.hasOwnProperty("label") ? labelEval : true) { + var entry = { + label: labelEval || 'Plot ' + (i + 1), + color: s.color, + options: { + lines: s.lines, + points: s.points, + bars: s.bars + } + } + validEntries.push(entry) + } + return validEntries; + }, []); + + // Sort the legend using either the default or a custom comparator + if (sorted) { + if ($.isFunction(sorted)) { + legendEntries.sort(sorted); + } else if (sorted === 'reverse') { + legendEntries.reverse(); + } else { + var ascending = (sorted !== 'descending'); + legendEntries.sort(function(a, b) { + return a.label === b.label + ? 0 + : ((a.label < b.label) !== ascending ? 1 : -1 // Logical XOR + ); + }); + } + } + + return legendEntries; + } + + // return false if opts1 same as opts2 + function checkOptions(opts1, opts2) { + for (var prop in opts1) { + if (opts1.hasOwnProperty(prop)) { + if (opts1[prop] !== opts2[prop]) { + return true; + } + } + } + return false; + } + + // Compare two lists of legend entries + function shouldRedraw(oldEntries, newEntries) { + if (!oldEntries || !newEntries) { + return true; + } + + if (oldEntries.length !== newEntries.length) { + return true; + } + var i, newEntry, oldEntry, newOpts, oldOpts; + for (i = 0; i < newEntries.length; i++) { + newEntry = newEntries[i]; + oldEntry = oldEntries[i]; + + if (newEntry.label !== oldEntry.label) { + return true; + } + + if (newEntry.color !== oldEntry.color) { + return true; + } + + // check for changes in lines options + newOpts = newEntry.options.lines; + oldOpts = oldEntry.options.lines; + if (checkOptions(newOpts, oldOpts)) { + return true; + } + + // check for changes in points options + newOpts = newEntry.options.points; + oldOpts = oldEntry.options.points; + if (checkOptions(newOpts, oldOpts)) { + return true; + } + + // check for changes in bars options + newOpts = newEntry.options.bars; + oldOpts = oldEntry.options.bars; + if (checkOptions(newOpts, oldOpts)) { + return true; + } + } + + return false; + } + + function init(plot) { + plot.hooks.setupGrid.push(function (plot) { + var options = plot.getOptions(); + var series = plot.getData(), + labelFormatter = options.legend.labelFormatter, + oldEntries = options.legend.legendEntries, + oldPlotOffset = options.legend.plotOffset, + newEntries = getLegendEntries(series, labelFormatter, options.legend.sorted), + newPlotOffset = plot.getPlotOffset(); + + if (shouldRedraw(oldEntries, newEntries) || + checkOptions(oldPlotOffset, newPlotOffset)) { + insertLegend(plot, options, plot.getPlaceholder(), newEntries); + } + }); + } + + $.plot.plugins.push({ + init: init, + options: defaultOptions, + name: 'legend', + version: '1.0' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.logaxis.js b/frontend/lib/flot/jquery.flot.logaxis.js new file mode 100644 index 0000000..2804aa1 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.logaxis.js @@ -0,0 +1,298 @@ +/* Pretty handling of log axes. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Copyright (c) 2015 Ciprian Ceteras cipix2000@gmail.com. +Copyright (c) 2017 Raluca Portase +Licensed under the MIT license. + +Set axis.mode to "log" to enable. +*/ + +/* global jQuery*/ + +/** +## jquery.flot.logaxis +This plugin is used to create logarithmic axis. This includes tick generation, +formatters and transformers to and from logarithmic representation. + +### Methods and hooks +*/ + +(function ($) { + 'use strict'; + + var options = { + xaxis: {} + }; + + /*tick generators and formatters*/ + var PREFERRED_LOG_TICK_VALUES = computePreferedLogTickValues(Number.MAX_VALUE, 10), + EXTENDED_LOG_TICK_VALUES = computePreferedLogTickValues(Number.MAX_VALUE, 4); + + function computePreferedLogTickValues(endLimit, rangeStep) { + var log10End = Math.floor(Math.log(endLimit) * Math.LOG10E) - 1, + log10Start = -log10End, + val, range, vals = []; + + for (var power = log10Start; power <= log10End; power++) { + range = parseFloat('1e' + power); + for (var mult = 1; mult < 9; mult += rangeStep) { + val = range * mult; + vals.push(val); + } + } + return vals; + } + + /** + - logTickGenerator(plot, axis, noTicks) + + Generates logarithmic ticks, depending on axis range. + In case the number of ticks that can be generated is less than the expected noTicks/4, + a linear tick generation is used. + */ + var logTickGenerator = function (plot, axis, noTicks) { + var ticks = [], + minIdx = -1, + maxIdx = -1, + surface = plot.getCanvas(), + logTickValues = PREFERRED_LOG_TICK_VALUES, + min = clampAxis(axis, plot), + max = axis.max; + + if (!noTicks) { + noTicks = 0.3 * Math.sqrt(axis.direction === "x" ? surface.width : surface.height); + } + + PREFERRED_LOG_TICK_VALUES.some(function (val, i) { + if (val >= min) { + minIdx = i; + return true; + } else { + return false; + } + }); + + PREFERRED_LOG_TICK_VALUES.some(function (val, i) { + if (val >= max) { + maxIdx = i; + return true; + } else { + return false; + } + }); + + if (maxIdx === -1) { + maxIdx = PREFERRED_LOG_TICK_VALUES.length - 1; + } + + if (maxIdx - minIdx <= noTicks / 4 && logTickValues.length !== EXTENDED_LOG_TICK_VALUES.length) { + //try with multiple of 5 for tick values + logTickValues = EXTENDED_LOG_TICK_VALUES; + minIdx *= 2; + maxIdx *= 2; + } + + var lastDisplayed = null, + inverseNoTicks = 1 / noTicks, + tickValue, pixelCoord, tick; + + // Count the number of tick values would appear, if we can get at least + // nTicks / 4 accept them. + if (maxIdx - minIdx >= noTicks / 4) { + for (var idx = maxIdx; idx >= minIdx; idx--) { + tickValue = logTickValues[idx]; + pixelCoord = (Math.log(tickValue) - Math.log(min)) / (Math.log(max) - Math.log(min)); + tick = tickValue; + + if (lastDisplayed === null) { + lastDisplayed = { + pixelCoord: pixelCoord, + idealPixelCoord: pixelCoord + }; + } else { + if (Math.abs(pixelCoord - lastDisplayed.pixelCoord) >= inverseNoTicks) { + lastDisplayed = { + pixelCoord: pixelCoord, + idealPixelCoord: lastDisplayed.idealPixelCoord - inverseNoTicks + }; + } else { + tick = null; + } + } + + if (tick) { + ticks.push(tick); + } + } + // Since we went in backwards order. + ticks.reverse(); + } else { + var tickSize = plot.computeTickSize(min, max, noTicks), + customAxis = {min: min, max: max, tickSize: tickSize}; + ticks = $.plot.linearTickGenerator(customAxis); + } + + return ticks; + }; + + var clampAxis = function (axis, plot) { + var min = axis.min, + max = axis.max; + + if (min <= 0) { + //for empty graph if axis.min is not strictly positive make it 0.1 + if (axis.datamin === null) { + min = axis.min = 0.1; + } else { + min = processAxisOffset(plot, axis); + } + + if (max < min) { + axis.max = axis.datamax !== null ? axis.datamax : axis.options.max; + axis.options.offset.below = 0; + axis.options.offset.above = 0; + } + } + + return min; + } + + /** + - logTickFormatter(value, axis, precision) + + This is the corresponding tickFormatter of the logaxis. + For a number greater that 10^6 or smaller than 10^(-3), this will be drawn + with e representation + */ + var logTickFormatter = function (value, axis, precision) { + var tenExponent = value > 0 ? Math.floor(Math.log(value) / Math.LN10) : 0; + + if (precision) { + if ((tenExponent >= -4) && (tenExponent <= 7)) { + return $.plot.defaultTickFormatter(value, axis, precision); + } else { + return $.plot.expRepTickFormatter(value, axis, precision); + } + } + if ((tenExponent >= -4) && (tenExponent <= 7)) { + //if we have float numbers, return a limited length string(ex: 0.0009 is represented as 0.000900001) + var formattedValue = tenExponent < 0 ? value.toFixed(-tenExponent) : value.toFixed(tenExponent + 2); + if (formattedValue.indexOf('.') !== -1) { + var lastZero = formattedValue.lastIndexOf('0'); + + while (lastZero === formattedValue.length - 1) { + formattedValue = formattedValue.slice(0, -1); + lastZero = formattedValue.lastIndexOf('0'); + } + + //delete the dot if is last + if (formattedValue.indexOf('.') === formattedValue.length - 1) { + formattedValue = formattedValue.slice(0, -1); + } + } + return formattedValue; + } else { + return $.plot.expRepTickFormatter(value, axis); + } + }; + + /*logaxis caracteristic functions*/ + var logTransform = function (v) { + if (v < PREFERRED_LOG_TICK_VALUES[0]) { + v = PREFERRED_LOG_TICK_VALUES[0]; + } + + return Math.log(v); + }; + + var logInverseTransform = function (v) { + return Math.exp(v); + }; + + var invertedTransform = function (v) { + return -v; + } + + var invertedLogTransform = function (v) { + return -logTransform(v); + } + + var invertedLogInverseTransform = function (v) { + return logInverseTransform(-v); + } + + /** + - setDataminRange(plot, axis) + + It is used for clamping the starting point of a logarithmic axis. + This will set the axis datamin range to 0.1 or to the first datapoint greater then 0. + The function is usefull since the logarithmic representation can not show + values less than or equal to 0. + */ + function setDataminRange(plot, axis) { + if (axis.options.mode === 'log' && axis.datamin <= 0) { + if (axis.datamin === null) { + axis.datamin = 0.1; + } else { + axis.datamin = processAxisOffset(plot, axis); + } + } + } + + function processAxisOffset(plot, axis) { + var series = plot.getData(), + range = series + .filter(function(series) { + return series.xaxis === axis || series.yaxis === axis; + }) + .map(function(series) { + return plot.computeRangeForDataSeries(series, null, isValid); + }), + min = axis.direction === 'x' + ? Math.min(0.1, range && range[0] ? range[0].xmin : 0.1) + : Math.min(0.1, range && range[0] ? range[0].ymin : 0.1); + + axis.min = min; + + return min; + } + + function isValid(a) { + return a > 0; + } + + function init(plot) { + plot.hooks.processOptions.push(function (plot) { + $.each(plot.getAxes(), function (axisName, axis) { + var opts = axis.options; + if (opts.mode === 'log') { + axis.tickGenerator = function (axis) { + var noTicks = 11; + return logTickGenerator(plot, axis, noTicks); + }; + if (typeof axis.options.tickFormatter !== 'function') { + axis.options.tickFormatter = logTickFormatter; + } + axis.options.transform = opts.inverted ? invertedLogTransform : logTransform; + axis.options.inverseTransform = opts.inverted ? invertedLogInverseTransform : logInverseTransform; + axis.options.autoScaleMargin = 0; + plot.hooks.setRange.push(setDataminRange); + } else if (opts.inverted) { + axis.options.transform = invertedTransform; + axis.options.inverseTransform = invertedTransform; + } + }); + }); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'log', + version: '0.1' + }); + + $.plot.logTicksGenerator = logTickGenerator; + $.plot.logTickFormatter = logTickFormatter; +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.navigate.js b/frontend/lib/flot/jquery.flot.navigate.js new file mode 100644 index 0000000..c1be1cf --- /dev/null +++ b/frontend/lib/flot/jquery.flot.navigate.js @@ -0,0 +1,798 @@ +/* Flot plugin for adding the ability to pan and zoom the plot. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Copyright (c) 2016 Ciprian Ceteras. +Copyright (c) 2017 Raluca Portase. +Licensed under the MIT license. + +*/ + +/** +## jquery.flot.navigate.js + +This flot plugin is used for adding the ability to pan and zoom the plot. +A higher level overview is available at [interactions](interactions.md) documentation. + +The default behaviour is scrollwheel up/down to zoom in, drag +to pan. The plugin defines plot.zoom({ center }), plot.zoomOut() and +plot.pan( offset ) so you easily can add custom controls. It also fires +"plotpan" and "plotzoom" events, useful for synchronizing plots. + +The plugin supports these options: +```js + zoom: { + interactive: false, + active: false, + amount: 1.5 // 2 = 200% (zoom in), 0.5 = 50% (zoom out) + } + + pan: { + interactive: false, + active: false, + cursor: "move", // CSS mouse cursor value used when dragging, e.g. "pointer" + frameRate: 60, + mode: "smart" // enable smart pan mode + } + + xaxis: { + axisZoom: true, //zoom axis when mouse over it is allowed + plotZoom: true, //zoom axis is allowed for plot zoom + axisPan: true, //pan axis when mouse over it is allowed + plotPan: true //pan axis is allowed for plot pan + } + + yaxis: { + axisZoom: true, //zoom axis when mouse over it is allowed + plotZoom: true, //zoom axis is allowed for plot zoom + axisPan: true, //pan axis when mouse over it is allowed + plotPan: true //pan axis is allowed for plot pan + } +``` +**interactive** enables the built-in drag/click behaviour. If you enable +interactive for pan, then you'll have a basic plot that supports moving +around; the same for zoom. + +**active** is true after a touch tap on plot. This enables plot navigation. +Once activated, zoom and pan cannot be deactivated. When the plot becomes active, +"plotactivated" event is triggered. + +**amount** specifies the default amount to zoom in (so 1.5 = 150%) relative to +the current viewport. + +**cursor** is a standard CSS mouse cursor string used for visual feedback to the +user when dragging. + +**frameRate** specifies the maximum number of times per second the plot will +update itself while the user is panning around on it (set to null to disable +intermediate pans, the plot will then not update until the mouse button is +released). + +**mode** a string specifies the pan mode for mouse interaction. Accepted values: +'manual': no pan hint or direction snapping; +'smart': The graph shows pan hint bar and the pan movement will snap +to one direction when the drag direction is close to it; +'smartLock'. The graph shows pan hint bar and the pan movement will always +snap to a direction that the drag diorection started with. + +Example API usage: +```js + plot = $.plot(...); + + // zoom default amount in on the pixel ( 10, 20 ) + plot.zoom({ center: { left: 10, top: 20 } }); + + // zoom out again + plot.zoomOut({ center: { left: 10, top: 20 } }); + + // zoom 200% in on the pixel (10, 20) + plot.zoom({ amount: 2, center: { left: 10, top: 20 } }); + + // pan 100 pixels to the left (changing x-range in a positive way) and 20 down + plot.pan({ left: -100, top: 20 }) +``` + +Here, "center" specifies where the center of the zooming should happen. Note +that this is defined in pixel space, not the space of the data points (you can +use the p2c helpers on the axes in Flot to help you convert between these). + +**amount** is the amount to zoom the viewport relative to the current range, so +1 is 100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out). You +can set the default in the options. +*/ + +/* eslint-enable */ +(function($) { + 'use strict'; + + var options = { + zoom: { + interactive: false, + active: false, + amount: 1.5 // how much to zoom relative to current position, 2 = 200% (zoom in), 0.5 = 50% (zoom out) + }, + pan: { + interactive: false, + active: false, + cursor: "move", + frameRate: 60, + mode: 'smart' + }, + recenter: { + interactive: true + }, + xaxis: { + axisZoom: true, //zoom axis when mouse over it is allowed + plotZoom: true, //zoom axis is allowed for plot zoom + axisPan: true, //pan axis when mouse over it is allowed + plotPan: true //pan axis is allowed for plot pan + }, + yaxis: { + axisZoom: true, + plotZoom: true, + axisPan: true, + plotPan: true + } + }; + + var saturated = $.plot.saturated; + var browser = $.plot.browser; + var SNAPPING_CONSTANT = $.plot.uiConstants.SNAPPING_CONSTANT; + var PANHINT_LENGTH_CONSTANT = $.plot.uiConstants.PANHINT_LENGTH_CONSTANT; + + function init(plot) { + plot.hooks.processOptions.push(initNevigation); + } + + function initNevigation(plot, options) { + var panAxes = null; + var canDrag = false; + var useManualPan = options.pan.mode === 'manual', + smartPanLock = options.pan.mode === 'smartLock', + useSmartPan = smartPanLock || options.pan.mode === 'smart'; + + function onZoomClick(e, zoomOut, amount) { + var page = browser.getPageXY(e); + + var c = plot.offset(); + c.left = page.X - c.left; + c.top = page.Y - c.top; + + var ec = plot.getPlaceholder().offset(); + ec.left = page.X - ec.left; + ec.top = page.Y - ec.top; + + var axes = plot.getXAxes().concat(plot.getYAxes()).filter(function (axis) { + var box = axis.box; + if (box !== undefined) { + return (ec.left > box.left) && (ec.left < box.left + box.width) && + (ec.top > box.top) && (ec.top < box.top + box.height); + } + }); + + if (axes.length === 0) { + axes = undefined; + } + + if (zoomOut) { + plot.zoomOut({ + center: c, + axes: axes, + amount: amount + }); + } else { + plot.zoom({ + center: c, + axes: axes, + amount: amount + }); + } + } + + var prevCursor = 'default', + panHint = null, + panTimeout = null, + plotState, + prevDragPosition = { x: 0, y: 0 }, + isPanAction = false; + + function onMouseWheel(e, delta) { + var maxAbsoluteDeltaOnMac = 1, + isMacScroll = Math.abs(e.originalEvent.deltaY) <= maxAbsoluteDeltaOnMac, + defaultNonMacScrollAmount = null, + macMagicRatio = 50, + amount = isMacScroll ? 1 + Math.abs(e.originalEvent.deltaY) / macMagicRatio : defaultNonMacScrollAmount; + + if (isPanAction) { + onDragEnd(e); + } + + if (plot.getOptions().zoom.active) { + e.preventDefault(); + onZoomClick(e, delta < 0, amount); + return false; + } + } + + plot.navigationState = function(startPageX, startPageY) { + var axes = this.getAxes(); + var result = {}; + Object.keys(axes).forEach(function(axisName) { + var axis = axes[axisName]; + result[axisName] = { + navigationOffset: { below: axis.options.offset.below || 0, + above: axis.options.offset.above || 0}, + axisMin: axis.min, + axisMax: axis.max, + diagMode: false + } + }); + + result.startPageX = startPageX || 0; + result.startPageY = startPageY || 0; + return result; + } + + function onMouseDown(e) { + canDrag = true; + } + + function onMouseUp(e) { + canDrag = false; + } + + function isLeftMouseButtonPressed(e) { + return e.button === 0; + } + + function onDragStart(e) { + if (!canDrag || !isLeftMouseButtonPressed(e)) { + return false; + } + + isPanAction = true; + var page = browser.getPageXY(e); + + var ec = plot.getPlaceholder().offset(); + ec.left = page.X - ec.left; + ec.top = page.Y - ec.top; + + panAxes = plot.getXAxes().concat(plot.getYAxes()).filter(function (axis) { + var box = axis.box; + if (box !== undefined) { + return (ec.left > box.left) && (ec.left < box.left + box.width) && + (ec.top > box.top) && (ec.top < box.top + box.height); + } + }); + + if (panAxes.length === 0) { + panAxes = undefined; + } + + var c = plot.getPlaceholder().css('cursor'); + if (c) { + prevCursor = c; + } + + plot.getPlaceholder().css('cursor', plot.getOptions().pan.cursor); + + if (useSmartPan) { + plotState = plot.navigationState(page.X, page.Y); + } else if (useManualPan) { + prevDragPosition.x = page.X; + prevDragPosition.y = page.Y; + } + } + + function onDrag(e) { + if (!isPanAction) { + return; + } + + var page = browser.getPageXY(e); + var frameRate = plot.getOptions().pan.frameRate; + + if (frameRate === -1) { + if (useSmartPan) { + plot.smartPan({ + x: plotState.startPageX - page.X, + y: plotState.startPageY - page.Y + }, plotState, panAxes, false, smartPanLock); + } else if (useManualPan) { + plot.pan({ + left: prevDragPosition.x - page.X, + top: prevDragPosition.y - page.Y, + axes: panAxes + }); + prevDragPosition.x = page.X; + prevDragPosition.y = page.Y; + } + return; + } + + if (panTimeout || !frameRate) return; + + panTimeout = setTimeout(function() { + if (useSmartPan) { + plot.smartPan({ + x: plotState.startPageX - page.X, + y: plotState.startPageY - page.Y + }, plotState, panAxes, false, smartPanLock); + } else if (useManualPan) { + plot.pan({ + left: prevDragPosition.x - page.X, + top: prevDragPosition.y - page.Y, + axes: panAxes + }); + prevDragPosition.x = page.X; + prevDragPosition.y = page.Y; + } + + panTimeout = null; + }, 1 / frameRate * 1000); + } + + function onDragEnd(e) { + if (!isPanAction) { + return; + } + + if (panTimeout) { + clearTimeout(panTimeout); + panTimeout = null; + } + + isPanAction = false; + var page = browser.getPageXY(e); + + plot.getPlaceholder().css('cursor', prevCursor); + + if (useSmartPan) { + plot.smartPan({ + x: plotState.startPageX - page.X, + y: plotState.startPageY - page.Y + }, plotState, panAxes, false, smartPanLock); + plot.smartPan.end(); + } else if (useManualPan) { + plot.pan({ + left: prevDragPosition.x - page.X, + top: prevDragPosition.y - page.Y, + axes: panAxes + }); + prevDragPosition.x = 0; + prevDragPosition.y = 0; + } + } + + function onDblClick(e) { + plot.activate(); + var o = plot.getOptions() + + if (!o.recenter.interactive) { return; } + + var axes = plot.getTouchedAxis(e.clientX, e.clientY), + event; + + plot.recenter({ axes: axes[0] ? axes : null }); + + if (axes[0]) { + event = new $.Event('re-center', { detail: { + axisTouched: axes[0] + }}); + } else { + event = new $.Event('re-center', { detail: e }); + } + plot.getPlaceholder().trigger(event); + } + + function onClick(e) { + plot.activate(); + + if (isPanAction) { + onDragEnd(e); + } + + return false; + } + + plot.activate = function() { + var o = plot.getOptions(); + if (!o.pan.active || !o.zoom.active) { + o.pan.active = true; + o.zoom.active = true; + plot.getPlaceholder().trigger("plotactivated", [plot]); + } + } + + function bindEvents(plot, eventHolder) { + var o = plot.getOptions(); + if (o.zoom.interactive) { + eventHolder.mousewheel(onMouseWheel); + } + + if (o.pan.interactive) { + plot.addEventHandler("dragstart", onDragStart, eventHolder, 0); + plot.addEventHandler("drag", onDrag, eventHolder, 0); + plot.addEventHandler("dragend", onDragEnd, eventHolder, 0); + eventHolder.bind("mousedown", onMouseDown); + eventHolder.bind("mouseup", onMouseUp); + } + + eventHolder.dblclick(onDblClick); + eventHolder.click(onClick); + } + + plot.zoomOut = function(args) { + if (!args) { + args = {}; + } + + if (!args.amount) { + args.amount = plot.getOptions().zoom.amount; + } + + args.amount = 1 / args.amount; + plot.zoom(args); + }; + + plot.zoom = function(args) { + if (!args) { + args = {}; + } + + var c = args.center, + amount = args.amount || plot.getOptions().zoom.amount, + w = plot.width(), + h = plot.height(), + axes = args.axes || plot.getAxes(); + + if (!c) { + c = { + left: w / 2, + top: h / 2 + }; + } + + var xf = c.left / w, + yf = c.top / h, + minmax = { + x: { + min: c.left - xf * w / amount, + max: c.left + (1 - xf) * w / amount + }, + y: { + min: c.top - yf * h / amount, + max: c.top + (1 - yf) * h / amount + } + }; + + for (var key in axes) { + if (!axes.hasOwnProperty(key)) { + continue; + } + + var axis = axes[key], + opts = axis.options, + min = minmax[axis.direction].min, + max = minmax[axis.direction].max, + navigationOffset = axis.options.offset; + + //skip axis without axisZoom when zooming only on certain axis or axis without plotZoom for zoom on entire plot + if ((!opts.axisZoom && args.axes) || (!args.axes && !opts.plotZoom)) { + continue; + } + + min = $.plot.saturated.saturate(axis.c2p(min)); + max = $.plot.saturated.saturate(axis.c2p(max)); + if (min > max) { + // make sure min < max + var tmp = min; + min = max; + max = tmp; + } + + var offsetBelow = $.plot.saturated.saturate(navigationOffset.below - (axis.min - min)); + var offsetAbove = $.plot.saturated.saturate(navigationOffset.above - (axis.max - max)); + opts.offset = { below: offsetBelow, above: offsetAbove }; + }; + + plot.setupGrid(true); + plot.draw(); + + if (!args.preventEvent) { + plot.getPlaceholder().trigger("plotzoom", [plot, args]); + } + }; + + plot.pan = function(args) { + var delta = { + x: +args.left, + y: +args.top + }; + + if (isNaN(delta.x)) delta.x = 0; + if (isNaN(delta.y)) delta.y = 0; + + $.each(args.axes || plot.getAxes(), function(_, axis) { + var opts = axis.options, + d = delta[axis.direction]; + + //skip axis without axisPan when panning only on certain axis or axis without plotPan for pan the entire plot + if ((!opts.axisPan && args.axes) || (!opts.plotPan && !args.axes)) { + return; + } + + if (d !== 0) { + var navigationOffsetBelow = saturated.saturate(axis.c2p(axis.p2c(axis.min) + d) - axis.c2p(axis.p2c(axis.min))), + navigationOffsetAbove = saturated.saturate(axis.c2p(axis.p2c(axis.max) + d) - axis.c2p(axis.p2c(axis.max))); + + if (!isFinite(navigationOffsetBelow)) { + navigationOffsetBelow = 0; + } + + if (!isFinite(navigationOffsetAbove)) { + navigationOffsetAbove = 0; + } + + opts.offset = { + below: saturated.saturate(navigationOffsetBelow + (opts.offset.below || 0)), + above: saturated.saturate(navigationOffsetAbove + (opts.offset.above || 0)) + }; + } + }); + + plot.setupGrid(true); + plot.draw(); + if (!args.preventEvent) { + plot.getPlaceholder().trigger("plotpan", [plot, args]); + } + }; + + plot.recenter = function(args) { + $.each(args.axes || plot.getAxes(), function(_, axis) { + if (args.axes) { + if (this.direction === 'x') { + axis.options.offset = { below: 0 }; + } else if (this.direction === 'y') { + axis.options.offset = { above: 0 }; + } + } else { + axis.options.offset = { below: 0, above: 0 }; + } + }); + plot.setupGrid(true); + plot.draw(); + }; + + var shouldSnap = function(delta) { + return (Math.abs(delta.y) < SNAPPING_CONSTANT && Math.abs(delta.x) >= SNAPPING_CONSTANT) || + (Math.abs(delta.x) < SNAPPING_CONSTANT && Math.abs(delta.y) >= SNAPPING_CONSTANT); + } + + // adjust delta so the pan action is constrained on the vertical or horizontal direction + // it the movements in the other direction are small + var adjustDeltaToSnap = function(delta) { + if (Math.abs(delta.x) < SNAPPING_CONSTANT && Math.abs(delta.y) >= SNAPPING_CONSTANT) { + return {x: 0, y: delta.y}; + } + + if (Math.abs(delta.y) < SNAPPING_CONSTANT && Math.abs(delta.x) >= SNAPPING_CONSTANT) { + return {x: delta.x, y: 0}; + } + + return delta; + } + + var lockedDirection = null; + var lockDeltaDirection = function(delta) { + if (!lockedDirection && Math.max(Math.abs(delta.x), Math.abs(delta.y)) >= SNAPPING_CONSTANT) { + lockedDirection = Math.abs(delta.x) < Math.abs(delta.y) ? 'y' : 'x'; + } + + switch (lockedDirection) { + case 'x': + return { x: delta.x, y: 0 }; + case 'y': + return { x: 0, y: delta.y }; + default: + return { x: 0, y: 0 }; + } + } + + var isDiagonalMode = function(delta) { + if (Math.abs(delta.x) > 0 && Math.abs(delta.y) > 0) { + return true; + } + return false; + } + + var restoreAxisOffset = function(axes, initialState, delta) { + var axis; + Object.keys(axes).forEach(function(axisName) { + axis = axes[axisName]; + if (delta[axis.direction] === 0) { + axis.options.offset.below = initialState[axisName].navigationOffset.below; + axis.options.offset.above = initialState[axisName].navigationOffset.above; + } + }); + } + + var prevDelta = { x: 0, y: 0 }; + plot.smartPan = function(delta, initialState, panAxes, preventEvent, smartLock) { + var snap = smartLock ? true : shouldSnap(delta), + axes = plot.getAxes(), + opts; + delta = smartLock ? lockDeltaDirection(delta) : adjustDeltaToSnap(delta); + + if (isDiagonalMode(delta)) { + initialState.diagMode = true; + } + + if (snap && initialState.diagMode === true) { + initialState.diagMode = false; + restoreAxisOffset(axes, initialState, delta); + } + + if (snap) { + panHint = { + start: { + x: initialState.startPageX - plot.offset().left + plot.getPlotOffset().left, + y: initialState.startPageY - plot.offset().top + plot.getPlotOffset().top + }, + end: { + x: initialState.startPageX - delta.x - plot.offset().left + plot.getPlotOffset().left, + y: initialState.startPageY - delta.y - plot.offset().top + plot.getPlotOffset().top + } + } + } else { + panHint = { + start: { + x: initialState.startPageX - plot.offset().left + plot.getPlotOffset().left, + y: initialState.startPageY - plot.offset().top + plot.getPlotOffset().top + }, + end: false + } + } + + if (isNaN(delta.x)) delta.x = 0; + if (isNaN(delta.y)) delta.y = 0; + + if (panAxes) { + axes = panAxes; + } + + var axis, axisMin, axisMax, p, d; + Object.keys(axes).forEach(function(axisName) { + axis = axes[axisName]; + axisMin = axis.min; + axisMax = axis.max; + opts = axis.options; + + d = delta[axis.direction]; + p = prevDelta[axis.direction]; + + //skip axis without axisPan when panning only on certain axis or axis without plotPan for pan the entire plot + if ((!opts.axisPan && panAxes) || (!panAxes && !opts.plotPan)) { + return; + } + + if (d !== 0) { + var navigationOffsetBelow = saturated.saturate(axis.c2p(axis.p2c(axisMin) - (p - d)) - axis.c2p(axis.p2c(axisMin))), + navigationOffsetAbove = saturated.saturate(axis.c2p(axis.p2c(axisMax) - (p - d)) - axis.c2p(axis.p2c(axisMax))); + + if (!isFinite(navigationOffsetBelow)) { + navigationOffsetBelow = 0; + } + + if (!isFinite(navigationOffsetAbove)) { + navigationOffsetAbove = 0; + } + + axis.options.offset.below = saturated.saturate(navigationOffsetBelow + (axis.options.offset.below || 0)); + axis.options.offset.above = saturated.saturate(navigationOffsetAbove + (axis.options.offset.above || 0)); + } + }); + + prevDelta = delta; + plot.setupGrid(true); + plot.draw(); + + if (!preventEvent) { + plot.getPlaceholder().trigger("plotpan", [plot, delta, panAxes, initialState]); + } + }; + + plot.smartPan.end = function() { + panHint = null; + lockedDirection = null; + prevDelta = { x: 0, y: 0 }; + plot.triggerRedrawOverlay(); + } + + function shutdown(plot, eventHolder) { + eventHolder.unbind("mousewheel", onMouseWheel); + eventHolder.unbind("mousedown", onMouseDown); + eventHolder.unbind("mouseup", onMouseUp); + eventHolder.unbind("dragstart", onDragStart); + eventHolder.unbind("drag", onDrag); + eventHolder.unbind("dragend", onDragEnd); + eventHolder.unbind("dblclick", onDblClick); + eventHolder.unbind("click", onClick); + + if (panTimeout) clearTimeout(panTimeout); + } + + function drawOverlay(plot, ctx) { + if (panHint) { + ctx.strokeStyle = 'rgba(96, 160, 208, 0.7)'; + ctx.lineWidth = 2; + ctx.lineJoin = "round"; + var startx = Math.round(panHint.start.x), + starty = Math.round(panHint.start.y), + endx, endy; + + if (panAxes) { + if (panAxes[0].direction === 'x') { + endy = Math.round(panHint.start.y); + endx = Math.round(panHint.end.x); + } else if (panAxes[0].direction === 'y') { + endx = Math.round(panHint.start.x); + endy = Math.round(panHint.end.y); + } + } else { + endx = Math.round(panHint.end.x); + endy = Math.round(panHint.end.y); + } + + ctx.beginPath(); + + if (panHint.end === false) { + ctx.moveTo(startx, starty - PANHINT_LENGTH_CONSTANT); + ctx.lineTo(startx, starty + PANHINT_LENGTH_CONSTANT); + + ctx.moveTo(startx + PANHINT_LENGTH_CONSTANT, starty); + ctx.lineTo(startx - PANHINT_LENGTH_CONSTANT, starty); + } else { + var dirX = starty === endy; + + ctx.moveTo(startx - (dirX ? 0 : PANHINT_LENGTH_CONSTANT), starty - (dirX ? PANHINT_LENGTH_CONSTANT : 0)); + ctx.lineTo(startx + (dirX ? 0 : PANHINT_LENGTH_CONSTANT), starty + (dirX ? PANHINT_LENGTH_CONSTANT : 0)); + + ctx.moveTo(startx, starty); + ctx.lineTo(endx, endy); + + ctx.moveTo(endx - (dirX ? 0 : PANHINT_LENGTH_CONSTANT), endy - (dirX ? PANHINT_LENGTH_CONSTANT : 0)); + ctx.lineTo(endx + (dirX ? 0 : PANHINT_LENGTH_CONSTANT), endy + (dirX ? PANHINT_LENGTH_CONSTANT : 0)); + } + + ctx.stroke(); + } + } + + plot.getTouchedAxis = function(touchPointX, touchPointY) { + var ec = plot.getPlaceholder().offset(); + ec.left = touchPointX - ec.left; + ec.top = touchPointY - ec.top; + + var axis = plot.getXAxes().concat(plot.getYAxes()).filter(function (axis) { + var box = axis.box; + if (box !== undefined) { + return (ec.left > box.left) && (ec.left < box.left + box.width) && + (ec.top > box.top) && (ec.top < box.top + box.height); + } + }); + + return axis; + } + + plot.hooks.drawOverlay.push(drawOverlay); + plot.hooks.bindEvents.push(bindEvents); + plot.hooks.shutdown.push(shutdown); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'navigate', + version: '1.3' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.pie.js b/frontend/lib/flot/jquery.flot.pie.js new file mode 100644 index 0000000..ec734a1 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.pie.js @@ -0,0 +1,786 @@ +/* Flot plugin for rendering pie charts. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin assumes that each series has a single data value, and that each +value is a positive integer or zero. Negative numbers don't make sense for a +pie chart, and have unpredictable results. The values do NOT need to be +passed in as percentages; the plugin will calculate the total and per-slice +percentages internally. + +* Created by Brian Medendorp + +* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars + +The plugin supports these options: + + series: { + pie: { + show: true/false + radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto' + innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect + startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result + tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show) + offset: { + top: integer value to move the pie up or down + left: integer value to move the pie left or right, or 'auto' + }, + stroke: { + color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF') + width: integer pixel width of the stroke + }, + label: { + show: true/false, or 'auto' + formatter: a user-defined function that modifies the text/style of the label text + radius: 0-1 for percentage of fullsize, or a specified pixel length + background: { + color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000') + opacity: 0-1 + }, + threshold: 0-1 for the percentage value at which to hide labels (if they're too small) + }, + combine: { + threshold: 0-1 for the percentage value at which to combine slices (if they're too small) + color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined + label: any text value of what the combined slice should be labeled + } + highlight: { + opacity: 0-1 + } + } + } + +More detail and specific examples can be found in the included HTML file. + +*/ + +(function($) { + // Maximum redraw attempts when fitting labels within the plot + + var REDRAW_ATTEMPTS = 10; + + // Factor by which to shrink the pie when fitting labels within the plot + + var REDRAW_SHRINK = 0.95; + + function init(plot) { + var canvas = null, + target = null, + options = null, + maxRadius = null, + centerLeft = null, + centerTop = null, + processed = false, + ctx = null; + + // interactive variables + + var highlights = []; + + // add hook to determine if pie plugin in enabled, and then perform necessary operations + + plot.hooks.processOptions.push(function(plot, options) { + if (options.series.pie.show) { + options.grid.show = false; + + // set labels.show + + if (options.series.pie.label.show === "auto") { + if (options.legend.show) { + options.series.pie.label.show = false; + } else { + options.series.pie.label.show = true; + } + } + + // set radius + + if (options.series.pie.radius === "auto") { + if (options.series.pie.label.show) { + options.series.pie.radius = 3 / 4; + } else { + options.series.pie.radius = 1; + } + } + + // ensure sane tilt + + if (options.series.pie.tilt > 1) { + options.series.pie.tilt = 1; + } else if (options.series.pie.tilt < 0) { + options.series.pie.tilt = 0; + } + } + }); + + plot.hooks.bindEvents.push(function(plot, eventHolder) { + var options = plot.getOptions(); + if (options.series.pie.show) { + if (options.grid.hoverable) { + eventHolder.unbind("mousemove").mousemove(onMouseMove); + } + if (options.grid.clickable) { + eventHolder.unbind("click").click(onClick); + } + } + }); + + plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) { + var options = plot.getOptions(); + if (options.series.pie.show) { + processDatapoints(plot, series, data, datapoints); + } + }); + + plot.hooks.drawOverlay.push(function(plot, octx) { + var options = plot.getOptions(); + if (options.series.pie.show) { + drawOverlay(plot, octx); + } + }); + + plot.hooks.draw.push(function(plot, newCtx) { + var options = plot.getOptions(); + if (options.series.pie.show) { + draw(plot, newCtx); + } + }); + + function processDatapoints(plot, series, datapoints) { + if (!processed) { + processed = true; + canvas = plot.getCanvas(); + target = $(canvas).parent(); + options = plot.getOptions(); + plot.setData(combine(plot.getData())); + } + } + + function combine(data) { + var total = 0, + combined = 0, + numCombined = 0, + color = options.series.pie.combine.color, + newdata = [], + i, + value; + + // Fix up the raw data from Flot, ensuring the data is numeric + + for (i = 0; i < data.length; ++i) { + value = data[i].data; + + // If the data is an array, we'll assume that it's a standard + // Flot x-y pair, and are concerned only with the second value. + + // Note how we use the original array, rather than creating a + // new one; this is more efficient and preserves any extra data + // that the user may have stored in higher indexes. + + if ($.isArray(value) && value.length === 1) { + value = value[0]; + } + + if ($.isArray(value)) { + // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 + if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { + value[1] = +value[1]; + } else { + value[1] = 0; + } + } else if (!isNaN(parseFloat(value)) && isFinite(value)) { + value = [1, +value]; + } else { + value = [1, 0]; + } + + data[i].data = [value]; + } + + // Sum up all the slices, so we can calculate percentages for each + + for (i = 0; i < data.length; ++i) { + total += data[i].data[0][1]; + } + + // Count the number of slices with percentages below the combine + // threshold; if it turns out to be just one, we won't combine. + + for (i = 0; i < data.length; ++i) { + value = data[i].data[0][1]; + if (value / total <= options.series.pie.combine.threshold) { + combined += value; + numCombined++; + if (!color) { + color = data[i].color; + } + } + } + + for (i = 0; i < data.length; ++i) { + value = data[i].data[0][1]; + if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { + newdata.push( + $.extend(data[i], { /* extend to allow keeping all other original data values + and using them e.g. in labelFormatter. */ + data: [[1, value]], + color: data[i].color, + label: data[i].label, + angle: value * Math.PI * 2 / total, + percent: value / (total / 100) + }) + ); + } + } + + if (numCombined > 1) { + newdata.push({ + data: [[1, combined]], + color: color, + label: options.series.pie.combine.label, + angle: combined * Math.PI * 2 / total, + percent: combined / (total / 100) + }); + } + + return newdata; + } + + function draw(plot, newCtx) { + if (!target) { + return; // if no series were passed + } + + var canvasWidth = plot.getPlaceholder().width(), + canvasHeight = plot.getPlaceholder().height(), + legendWidth = target.children().filter(".legend").children().width() || 0; + + ctx = newCtx; + + // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! + + // When combining smaller slices into an 'other' slice, we need to + // add a new series. Since Flot gives plugins no way to modify the + // list of series, the pie plugin uses a hack where the first call + // to processDatapoints results in a call to setData with the new + // list of series, then subsequent processDatapoints do nothing. + + // The plugin-global 'processed' flag is used to control this hack; + // it starts out false, and is set to true after the first call to + // processDatapoints. + + // Unfortunately this turns future setData calls into no-ops; they + // call processDatapoints, the flag is true, and nothing happens. + + // To fix this we'll set the flag back to false here in draw, when + // all series have been processed, so the next sequence of calls to + // processDatapoints once again starts out with a slice-combine. + // This is really a hack; in 0.9 we need to give plugins a proper + // way to modify series before any processing begins. + + processed = false; + + // calculate maximum radius and center point + maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; + centerTop = canvasHeight / 2 + options.series.pie.offset.top; + centerLeft = canvasWidth / 2; + + if (options.series.pie.offset.left === "auto") { + if (options.legend.position.match("w")) { + centerLeft += legendWidth / 2; + } else { + centerLeft -= legendWidth / 2; + } + if (centerLeft < maxRadius) { + centerLeft = maxRadius; + } else if (centerLeft > canvasWidth - maxRadius) { + centerLeft = canvasWidth - maxRadius; + } + } else { + centerLeft += options.series.pie.offset.left; + } + + var slices = plot.getData(), + attempts = 0; + + // Keep shrinking the pie's radius until drawPie returns true, + // indicating that all the labels fit, or we try too many times. + do { + if (attempts > 0) { + maxRadius *= REDRAW_SHRINK; + } + attempts += 1; + clear(); + if (options.series.pie.tilt <= 0.8) { + drawShadow(); + } + } while (!drawPie() && attempts < REDRAW_ATTEMPTS) + + if (attempts >= REDRAW_ATTEMPTS) { + clear(); + target.prepend("
Could not draw pie with labels contained inside canvas
"); + } + + if (plot.setSeries && plot.insertLegend) { + plot.setSeries(slices); + plot.insertLegend(); + } + + // we're actually done at this point, just defining internal functions at this point + function clear() { + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + target.children().filter(".pieLabel, .pieLabelBackground").remove(); + } + + function drawShadow() { + var shadowLeft = options.series.pie.shadow.left; + var shadowTop = options.series.pie.shadow.top; + var edge = 10; + var alpha = options.series.pie.shadow.alpha; + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + + if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) { + return; // shadow would be outside canvas, so don't draw it + } + + ctx.save(); + ctx.translate(shadowLeft, shadowTop); + ctx.globalAlpha = alpha; + ctx.fillStyle = "#000"; + + // center and rotate to starting position + ctx.translate(centerLeft, centerTop); + ctx.scale(1, options.series.pie.tilt); + + //radius -= edge; + for (var i = 1; i <= edge; i++) { + ctx.beginPath(); + ctx.arc(0, 0, radius, 0, Math.PI * 2, false); + ctx.fill(); + radius -= i; + } + + ctx.restore(); + } + + function drawPie() { + var startAngle = Math.PI * options.series.pie.startAngle; + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + var i; + // center and rotate to starting position + + ctx.save(); + ctx.translate(centerLeft, centerTop); + ctx.scale(1, options.series.pie.tilt); + //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera + + // draw slices + ctx.save(); + + var currentAngle = startAngle; + for (i = 0; i < slices.length; ++i) { + slices[i].startAngle = currentAngle; + drawSlice(slices[i].angle, slices[i].color, true); + } + + ctx.restore(); + + // draw slice outlines + if (options.series.pie.stroke.width > 0) { + ctx.save(); + ctx.lineWidth = options.series.pie.stroke.width; + currentAngle = startAngle; + for (i = 0; i < slices.length; ++i) { + drawSlice(slices[i].angle, options.series.pie.stroke.color, false); + } + + ctx.restore(); + } + + // draw donut hole + drawDonutHole(ctx); + + ctx.restore(); + + // Draw the labels, returning true if they fit within the plot + if (options.series.pie.label.show) { + return drawLabels(); + } else return true; + + function drawSlice(angle, color, fill) { + if (angle <= 0 || isNaN(angle)) { + return; + } + + if (fill) { + ctx.fillStyle = color; + } else { + ctx.strokeStyle = color; + ctx.lineJoin = "round"; + } + + ctx.beginPath(); + if (Math.abs(angle - Math.PI * 2) > 0.000000001) { + ctx.moveTo(0, 0); // Center of the pie + } + + //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera + ctx.arc(0, 0, radius, currentAngle, currentAngle + angle / 2, false); + ctx.arc(0, 0, radius, currentAngle + angle / 2, currentAngle + angle, false); + ctx.closePath(); + //ctx.rotate(angle); // This doesn't work properly in Opera + currentAngle += angle; + + if (fill) { + ctx.fill(); + } else { + ctx.stroke(); + } + } + + function drawLabels() { + var currentAngle = startAngle; + var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius; + + for (var i = 0; i < slices.length; ++i) { + if (slices[i].percent >= options.series.pie.label.threshold * 100) { + if (!drawLabel(slices[i], currentAngle, i)) { + return false; + } + } + currentAngle += slices[i].angle; + } + + return true; + + function drawLabel(slice, startAngle, index) { + if (slice.data[0][1] === 0) { + return true; + } + + // format label text + var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; + + if (lf) { + text = lf(slice.label, slice); + } else { + text = slice.label; + } + + if (plf) { + text = plf(text, slice); + } + + var halfAngle = ((startAngle + slice.angle) + startAngle) / 2; + var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); + var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; + + var html = "" + text + ""; + target.append(html); + + var label = target.children("#pieLabel" + index); + var labelTop = (y - label.height() / 2); + var labelLeft = (x - label.width() / 2); + + label.css("top", labelTop); + label.css("left", labelLeft); + + // check to make sure that the label is not outside the canvas + if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { + return false; + } + + if (options.series.pie.label.background.opacity !== 0) { + // put in the transparent background separately to avoid blended labels and label boxes + var c = options.series.pie.label.background.color; + if (c == null) { + c = slice.color; + } + + var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;"; + $("
") + .css("opacity", options.series.pie.label.background.opacity) + .insertBefore(label); + } + + return true; + } // end individual label function + } // end drawLabels function + } // end drawPie function + } // end draw function + + // Placed here because it needs to be accessed from multiple locations + + function drawDonutHole(layer) { + if (options.series.pie.innerRadius > 0) { + // subtract the center + layer.save(); + var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; + layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color + layer.beginPath(); + layer.fillStyle = options.series.pie.stroke.color; + layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); + layer.fill(); + layer.closePath(); + layer.restore(); + + // add inner stroke + layer.save(); + layer.beginPath(); + layer.strokeStyle = options.series.pie.stroke.color; + layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); + layer.stroke(); + layer.closePath(); + layer.restore(); + + // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. + } + } + + //-- Additional Interactive related functions -- + + function isPointInPoly(poly, pt) { + for (var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) { + ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || + (poly[j][1] <= pt[1] && pt[1] < poly[i][1])) && + (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) && + (c = !c); + } + return c; + } + + function findNearbySlice(mouseX, mouseY) { + var slices = plot.getData(), + options = plot.getOptions(), + radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius, + x, y; + + for (var i = 0; i < slices.length; ++i) { + var s = slices[i]; + if (s.pie.show) { + ctx.save(); + ctx.beginPath(); + ctx.moveTo(0, 0); // Center of the pie + //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. + ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); + ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); + ctx.closePath(); + x = mouseX - centerLeft; + y = mouseY - centerTop; + + if (ctx.isPointInPath) { + if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { + ctx.restore(); + return { + datapoint: [s.percent, s.data], + dataIndex: 0, + series: s, + seriesIndex: i + }; + } + } else { + // excanvas for IE doesn;t support isPointInPath, this is a workaround. + var p1X = radius * Math.cos(s.startAngle), + p1Y = radius * Math.sin(s.startAngle), + p2X = radius * Math.cos(s.startAngle + s.angle / 4), + p2Y = radius * Math.sin(s.startAngle + s.angle / 4), + p3X = radius * Math.cos(s.startAngle + s.angle / 2), + p3Y = radius * Math.sin(s.startAngle + s.angle / 2), + p4X = radius * Math.cos(s.startAngle + s.angle / 1.5), + p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5), + p5X = radius * Math.cos(s.startAngle + s.angle), + p5Y = radius * Math.sin(s.startAngle + s.angle), + arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]], + arrPoint = [x, y]; + + // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt? + + if (isPointInPoly(arrPoly, arrPoint)) { + ctx.restore(); + return { + datapoint: [s.percent, s.data], + dataIndex: 0, + series: s, + seriesIndex: i + }; + } + } + + ctx.restore(); + } + } + + return null; + } + + function onMouseMove(e) { + triggerClickHoverEvent("plothover", e); + } + + function onClick(e) { + triggerClickHoverEvent("plotclick", e); + } + + // trigger click or hover event (they send the same parameters so we share their code) + + function triggerClickHoverEvent(eventname, e) { + var offset = plot.offset(); + var canvasX = parseInt(e.pageX - offset.left); + var canvasY = parseInt(e.pageY - offset.top); + var item = findNearbySlice(canvasX, canvasY); + + if (options.grid.autoHighlight) { + // clear auto-highlights + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.auto === eventname && !(item && h.series === item.series)) { + unhighlight(h.series); + } + } + } + + // highlight the slice + + if (item) { + highlight(item.series, eventname); + } + + // trigger any hover bind events + + var pos = { pageX: e.pageX, pageY: e.pageY }; + target.trigger(eventname, [pos, item]); + } + + function highlight(s, auto) { + //if (typeof s == "number") { + // s = series[s]; + //} + + var i = indexOfHighlight(s); + + if (i === -1) { + highlights.push({ series: s, auto: auto }); + plot.triggerRedrawOverlay(); + } else if (!auto) { + highlights[i].auto = false; + } + } + + function unhighlight(s) { + if (s == null) { + highlights = []; + plot.triggerRedrawOverlay(); + } + + //if (typeof s == "number") { + // s = series[s]; + //} + + var i = indexOfHighlight(s); + + if (i !== -1) { + highlights.splice(i, 1); + plot.triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s) { + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.series === s) { + return i; + } + } + return -1; + } + + function drawOverlay(plot, octx) { + var options = plot.getOptions(); + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + + octx.save(); + octx.translate(centerLeft, centerTop); + octx.scale(1, options.series.pie.tilt); + + for (var i = 0; i < highlights.length; ++i) { + drawHighlight(highlights[i].series); + } + + drawDonutHole(octx); + + octx.restore(); + + function drawHighlight(series) { + if (series.angle <= 0 || isNaN(series.angle)) { + return; + } + + //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); + octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor + octx.beginPath(); + if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { + octx.moveTo(0, 0); // Center of the pie + } + octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); + octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false); + octx.closePath(); + octx.fill(); + } + } + } // end init (plugin body) + + // define pie specific options and their default values + var options = { + series: { + pie: { + show: false, + radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) + innerRadius: 0, /* for donut */ + startAngle: 3 / 2, + tilt: 1, + shadow: { + left: 5, // shadow left offset + top: 15, // shadow top offset + alpha: 0.02 // shadow alpha + }, + offset: { + top: 0, + left: "auto" + }, + stroke: { + color: "#fff", + width: 1 + }, + label: { + show: "auto", + formatter: function(label, slice) { + return "
" + label + "
" + Math.round(slice.percent) + "%
"; + }, // formatter function + radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) + background: { + color: null, + opacity: 0 + }, + threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) + }, + combine: { + threshold: -1, // percentage at which to combine little slices into one larger slice + color: null, // color to give the new slice (auto-generated if null) + label: "Other" // label to give the new slice + }, + highlight: { + //color: "#fff", // will add this functionality once parseColor is available + opacity: 0.5 + } + } + } + }; + + $.plot.plugins.push({ + init: init, + options: options, + name: "pie", + version: "1.1" + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.resize.js b/frontend/lib/flot/jquery.flot.resize.js new file mode 100644 index 0000000..930c68e --- /dev/null +++ b/frontend/lib/flot/jquery.flot.resize.js @@ -0,0 +1,60 @@ +/* eslint-disable */ +/* Flot plugin for automatically redrawing plots as the placeholder resizes. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +It works by listening for changes on the placeholder div (through the jQuery +resize event plugin) - if the size changes, it will redraw the plot. + +There are no options. If you need to disable the plugin for some plots, you +can just fix the size of their placeholders. + +*/ + +/* Inline dependency: + * jQuery resize event - v1.1 - 3/14/2010 + * http://benalman.com/projects/jquery-resize-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ +(function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this); + +/* eslint-enable */ +(function ($) { + var options = { }; // no options + + function init(plot) { + function onResize() { + var placeholder = plot.getPlaceholder(); + + // somebody might have hidden us and we can't plot + // when we don't have the dimensions + if (placeholder.width() === 0 || placeholder.height() === 0) return; + + plot.resize(); + plot.setupGrid(); + plot.draw(); + } + + function bindEvents(plot, eventHolder) { + plot.getPlaceholder().resize(onResize); + } + + function shutdown(plot, eventHolder) { + plot.getPlaceholder().unbind("resize", onResize); + } + + plot.hooks.bindEvents.push(bindEvents); + plot.hooks.shutdown.push(shutdown); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'resize', + version: '1.0' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.saturated.js b/frontend/lib/flot/jquery.flot.saturated.js new file mode 100644 index 0000000..34b9c50 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.saturated.js @@ -0,0 +1,43 @@ +(function ($) { + 'use strict'; + var saturated = { + saturate: function (a) { + if (a === Infinity) { + return Number.MAX_VALUE; + } + + if (a === -Infinity) { + return -Number.MAX_VALUE; + } + + return a; + }, + delta: function(min, max, noTicks) { + return ((max - min) / noTicks) === Infinity ? (max / noTicks - min / noTicks) : (max - min) / noTicks + }, + multiply: function (a, b) { + return saturated.saturate(a * b); + }, + // returns c * bInt * a. Beahves properly in the case where c is negative + // and bInt * a is bigger that Number.MAX_VALUE (Infinity) + multiplyAdd: function (a, bInt, c) { + if (isFinite(a * bInt)) { + return saturated.saturate(a * bInt + c); + } else { + var result = c; + + for (var i = 0; i < bInt; i++) { + result += a; + } + + return saturated.saturate(result); + } + }, + // round to nearby lower multiple of base + floorInBase: function(n, base) { + return base * Math.floor(n / base); + } + }; + + $.plot.saturated = saturated; +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.selection.js b/frontend/lib/flot/jquery.flot.selection.js new file mode 100644 index 0000000..9c9fada --- /dev/null +++ b/frontend/lib/flot/jquery.flot.selection.js @@ -0,0 +1,517 @@ +/* Flot plugin for selecting regions of a plot. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin supports these options: + +selection: { + mode: null or "x" or "y" or "xy" or "smart", + color: color, + shape: "round" or "miter" or "bevel", + visualization: "fill" or "focus", + minSize: number of pixels +} + +Selection support is enabled by setting the mode to one of "x", "y" or "xy". +In "x" mode, the user will only be able to specify the x range, similarly for +"y" mode. For "xy", the selection becomes a rectangle where both ranges can be +specified. "color" is color of the selection (if you need to change the color +later on, you can get to it with plot.getOptions().selection.color). "shape" +is the shape of the corners of the selection. + +The way how the selection is visualized, can be changed by using the option +"visualization". Flot currently supports two modes: "focus" and "fill". The +option "focus" draws a colored bezel around the selected area while keeping +the selected area clear. The option "fill" highlights (i.e., fills) the +selected area with a colored highlight. + +"minSize" is the minimum size a selection can be in pixels. This value can +be customized to determine the smallest size a selection can be and still +have the selection rectangle be displayed. When customizing this value, the +fact that it refers to pixels, not axis units must be taken into account. +Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 +minute, setting "minSize" to 1 will not make the minimum selection size 1 +minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent +"plotunselected" events from being fired when the user clicks the mouse without +dragging. + +When selection support is enabled, a "plotselected" event will be emitted on +the DOM element you passed into the plot function. The event handler gets a +parameter with the ranges selected on the axes, like this: + + placeholder.bind( "plotselected", function( event, ranges ) { + alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) + // similar for yaxis - with multiple axes, the extra ones are in + // x2axis, x3axis, ... + }); + +The "plotselected" event is only fired when the user has finished making the +selection. A "plotselecting" event is fired during the process with the same +parameters as the "plotselected" event, in case you want to know what's +happening while it's happening, + +A "plotunselected" event with no arguments is emitted when the user clicks the +mouse to remove the selection. As stated above, setting "minSize" to 0 will +destroy this behavior. + +The plugin allso adds the following methods to the plot object: + +- setSelection( ranges, preventEvent ) + + Set the selection rectangle. The passed in ranges is on the same form as + returned in the "plotselected" event. If the selection mode is "x", you + should put in either an xaxis range, if the mode is "y" you need to put in + an yaxis range and both xaxis and yaxis if the selection mode is "xy", like + this: + + setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); + + setSelection will trigger the "plotselected" event when called. If you don't + want that to happen, e.g. if you're inside a "plotselected" handler, pass + true as the second parameter. If you are using multiple axes, you can + specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of + xaxis, the plugin picks the first one it sees. + +- clearSelection( preventEvent ) + + Clear the selection rectangle. Pass in true to avoid getting a + "plotunselected" event. + +- getSelection() + + Returns the current selection in the same format as the "plotselected" + event. If there's currently no selection, the function returns null. + +*/ + +(function ($) { + function init(plot) { + var selection = { + first: {x: -1, y: -1}, + second: {x: -1, y: -1}, + show: false, + currentMode: 'xy', + active: false + }; + + var SNAPPING_CONSTANT = $.plot.uiConstants.SNAPPING_CONSTANT; + + // FIXME: The drag handling implemented here should be + // abstracted out, there's some similar code from a library in + // the navigation plugin, this should be massaged a bit to fit + // the Flot cases here better and reused. Doing this would + // make this plugin much slimmer. + var savedhandlers = {}; + + function onDrag(e) { + if (selection.active) { + updateSelection(e); + + plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); + } + } + + function onDragStart(e) { + var o = plot.getOptions(); + // only accept left-click + if (e.which !== 1 || o.selection.mode === null) return; + + // reinitialize currentMode + selection.currentMode = 'xy'; + + // cancel out any text selections + document.body.focus(); + + // prevent text selection and drag in old-school browsers + if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { + savedhandlers.onselectstart = document.onselectstart; + document.onselectstart = function () { return false; }; + } + if (document.ondrag !== undefined && savedhandlers.ondrag == null) { + savedhandlers.ondrag = document.ondrag; + document.ondrag = function () { return false; }; + } + + setSelectionPos(selection.first, e); + + selection.active = true; + } + + function onDragEnd(e) { + // revert drag stuff for old-school browsers + if (document.onselectstart !== undefined) { + document.onselectstart = savedhandlers.onselectstart; + } + + if (document.ondrag !== undefined) { + document.ondrag = savedhandlers.ondrag; + } + + // no more dragging + selection.active = false; + updateSelection(e); + + if (selectionIsSane()) { + triggerSelectedEvent(); + } else { + // this counts as a clear + plot.getPlaceholder().trigger("plotunselected", [ ]); + plot.getPlaceholder().trigger("plotselecting", [ null ]); + } + + return false; + } + + function getSelection() { + if (!selectionIsSane()) return null; + + if (!selection.show) return null; + + var r = {}, + c1 = {x: selection.first.x, y: selection.first.y}, + c2 = {x: selection.second.x, y: selection.second.y}; + + if (selectionDirection(plot) === 'x') { + c1.y = 0; + c2.y = plot.height(); + } + + if (selectionDirection(plot) === 'y') { + c1.x = 0; + c2.x = plot.width(); + } + + $.each(plot.getAxes(), function (name, axis) { + if (axis.used) { + var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); + r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; + } + }); + return r; + } + + function triggerSelectedEvent() { + var r = getSelection(); + + plot.getPlaceholder().trigger("plotselected", [ r ]); + + // backwards-compat stuff, to be removed in future + if (r.xaxis && r.yaxis) { + plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); + } + } + + function clamp(min, value, max) { + return value < min ? min : (value > max ? max : value); + } + + function selectionDirection(plot) { + var o = plot.getOptions(); + + if (o.selection.mode === 'smart') { + return selection.currentMode; + } else { + return o.selection.mode; + } + } + + function updateMode(pos) { + if (selection.first) { + var delta = { + x: pos.x - selection.first.x, + y: pos.y - selection.first.y + }; + + if (Math.abs(delta.x) < SNAPPING_CONSTANT) { + selection.currentMode = 'y'; + } else if (Math.abs(delta.y) < SNAPPING_CONSTANT) { + selection.currentMode = 'x'; + } else { + selection.currentMode = 'xy'; + } + } + } + + function setSelectionPos(pos, e) { + var offset = plot.getPlaceholder().offset(); + var plotOffset = plot.getPlotOffset(); + pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); + pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); + + if (pos !== selection.first) updateMode(pos); + + if (selectionDirection(plot) === "y") { + pos.x = pos === selection.first ? 0 : plot.width(); + } + + if (selectionDirection(plot) === "x") { + pos.y = pos === selection.first ? 0 : plot.height(); + } + } + + function updateSelection(pos) { + if (pos.pageX == null) return; + + setSelectionPos(selection.second, pos); + if (selectionIsSane()) { + selection.show = true; + plot.triggerRedrawOverlay(); + } else clearSelection(true); + } + + function clearSelection(preventEvent) { + if (selection.show) { + selection.show = false; + selection.currentMode = ''; + plot.triggerRedrawOverlay(); + if (!preventEvent) { + plot.getPlaceholder().trigger("plotunselected", [ ]); + } + } + } + + // function taken from markings support in Flot + function extractRange(ranges, coord) { + var axis, from, to, key, axes = plot.getAxes(); + + for (var k in axes) { + axis = axes[k]; + if (axis.direction === coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n === 1) { + // support x1axis as xaxis + key = coord + "axis"; + } + + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } + } + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord === "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } + + return { from: from, to: to, axis: axis }; + } + + function setSelection(ranges, preventEvent) { + var range; + + if (selectionDirection(plot) === "y") { + selection.first.x = 0; + selection.second.x = plot.width(); + } else { + range = extractRange(ranges, "x"); + selection.first.x = range.axis.p2c(range.from); + selection.second.x = range.axis.p2c(range.to); + } + + if (selectionDirection(plot) === "x") { + selection.first.y = 0; + selection.second.y = plot.height(); + } else { + range = extractRange(ranges, "y"); + selection.first.y = range.axis.p2c(range.from); + selection.second.y = range.axis.p2c(range.to); + } + + selection.show = true; + plot.triggerRedrawOverlay(); + if (!preventEvent && selectionIsSane()) { + triggerSelectedEvent(); + } + } + + function selectionIsSane() { + var minSize = plot.getOptions().selection.minSize; + return Math.abs(selection.second.x - selection.first.x) >= minSize && + Math.abs(selection.second.y - selection.first.y) >= minSize; + } + + plot.clearSelection = clearSelection; + plot.setSelection = setSelection; + plot.getSelection = getSelection; + + plot.hooks.bindEvents.push(function(plot, eventHolder) { + var o = plot.getOptions(); + if (o.selection.mode != null) { + plot.addEventHandler("dragstart", onDragStart, eventHolder, 0); + plot.addEventHandler("drag", onDrag, eventHolder, 0); + plot.addEventHandler("dragend", onDragEnd, eventHolder, 0); + } + }); + + function drawSelectionDecorations(ctx, x, y, w, h, oX, oY, mode) { + var spacing = 3; + var fullEarWidth = 15; + var earWidth = Math.max(0, Math.min(fullEarWidth, w / 2 - 2, h / 2 - 2)); + ctx.fillStyle = '#ffffff'; + + if (mode === 'xy') { + ctx.beginPath(); + ctx.moveTo(x, y + earWidth); + ctx.lineTo(x - 3, y + earWidth); + ctx.lineTo(x - 3, y - 3); + ctx.lineTo(x + earWidth, y - 3); + ctx.lineTo(x + earWidth, y); + ctx.lineTo(x, y); + ctx.closePath(); + + ctx.moveTo(x, y + h - earWidth); + ctx.lineTo(x - 3, y + h - earWidth); + ctx.lineTo(x - 3, y + h + 3); + ctx.lineTo(x + earWidth, y + h + 3); + ctx.lineTo(x + earWidth, y + h); + ctx.lineTo(x, y + h); + ctx.closePath(); + + ctx.moveTo(x + w, y + earWidth); + ctx.lineTo(x + w + 3, y + earWidth); + ctx.lineTo(x + w + 3, y - 3); + ctx.lineTo(x + w - earWidth, y - 3); + ctx.lineTo(x + w - earWidth, y); + ctx.lineTo(x + w, y); + ctx.closePath(); + + ctx.moveTo(x + w, y + h - earWidth); + ctx.lineTo(x + w + 3, y + h - earWidth); + ctx.lineTo(x + w + 3, y + h + 3); + ctx.lineTo(x + w - earWidth, y + h + 3); + ctx.lineTo(x + w - earWidth, y + h); + ctx.lineTo(x + w, y + h); + ctx.closePath(); + + ctx.stroke(); + ctx.fill(); + } + + x = oX; + y = oY; + + if (mode === 'x') { + ctx.beginPath(); + ctx.moveTo(x, y + fullEarWidth); + ctx.lineTo(x, y - fullEarWidth); + ctx.lineTo(x - spacing, y - fullEarWidth); + ctx.lineTo(x - spacing, y + fullEarWidth); + ctx.closePath(); + + ctx.moveTo(x + w, y + fullEarWidth); + ctx.lineTo(x + w, y - fullEarWidth); + ctx.lineTo(x + w + spacing, y - fullEarWidth); + ctx.lineTo(x + w + spacing, y + fullEarWidth); + ctx.closePath(); + ctx.stroke(); + ctx.fill(); + } + + if (mode === 'y') { + ctx.beginPath(); + + ctx.moveTo(x - fullEarWidth, y); + ctx.lineTo(x + fullEarWidth, y); + ctx.lineTo(x + fullEarWidth, y - spacing); + ctx.lineTo(x - fullEarWidth, y - spacing); + ctx.closePath(); + + ctx.moveTo(x - fullEarWidth, y + h); + ctx.lineTo(x + fullEarWidth, y + h); + ctx.lineTo(x + fullEarWidth, y + h + spacing); + ctx.lineTo(x - fullEarWidth, y + h + spacing); + ctx.closePath(); + ctx.stroke(); + ctx.fill(); + } + } + + plot.hooks.drawOverlay.push(function (plot, ctx) { + // draw selection + if (selection.show && selectionIsSane()) { + var plotOffset = plot.getPlotOffset(); + var o = plot.getOptions(); + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var c = $.color.parse(o.selection.color); + var visualization = o.selection.visualization; + + var scalingFactor = 1; + + // use a dimmer scaling factor if visualization is "fill" + if (visualization === "fill") { + scalingFactor = 0.8; + } + + ctx.strokeStyle = c.scale('a', scalingFactor).toString(); + ctx.lineWidth = 1; + ctx.lineJoin = o.selection.shape; + ctx.fillStyle = c.scale('a', 0.4).toString(); + + var x = Math.min(selection.first.x, selection.second.x) + 0.5, + oX = x, + y = Math.min(selection.first.y, selection.second.y) + 0.5, + oY = y, + w = Math.abs(selection.second.x - selection.first.x) - 1, + h = Math.abs(selection.second.y - selection.first.y) - 1; + + if (selectionDirection(plot) === 'x') { + h += y; + y = 0; + } + + if (selectionDirection(plot) === 'y') { + w += x; + x = 0; + } + + if (visualization === "fill") { + ctx.fillRect(x, y, w, h); + ctx.strokeRect(x, y, w, h); + } else { + ctx.fillRect(0, 0, plot.width(), plot.height()); + ctx.clearRect(x, y, w, h); + drawSelectionDecorations(ctx, x, y, w, h, oX, oY, selectionDirection(plot)); + } + + ctx.restore(); + } + }); + + plot.hooks.shutdown.push(function (plot, eventHolder) { + eventHolder.unbind("dragstart", onDragStart); + eventHolder.unbind("drag", onDrag); + eventHolder.unbind("dragend", onDragEnd); + }); + } + + $.plot.plugins.push({ + init: init, + options: { + selection: { + mode: null, // one of null, "x", "y" or "xy" + visualization: "focus", // "focus" or "fill" + color: "#888888", + shape: "round", // one of "round", "miter", or "bevel" + minSize: 5 // minimum number of pixels + } + }, + name: 'selection', + version: '1.1' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.stack.js b/frontend/lib/flot/jquery.flot.stack.js new file mode 100644 index 0000000..f4b88e7 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.stack.js @@ -0,0 +1,220 @@ +/* Flot plugin for stacking data sets rather than overlaying them. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin assumes the data is sorted on x (or y if stacking horizontally). +For line charts, it is assumed that if a line has an undefined gap (from a +null point), then the line above it should have the same gap - insert zeros +instead of "null" if you want another behaviour. This also holds for the start +and end of the chart. Note that stacking a mix of positive and negative values +in most instances doesn't make sense (so it looks weird). + +Two or more series are stacked when their "stack" attribute is set to the same +key (which can be any number or string or just "true"). To specify the default +stack, you can set the stack option like this: + + series: { + stack: null/false, true, or a key (number/string) + } + +You can also specify it for a single series, like this: + + $.plot( $("#placeholder"), [{ + data: [ ... ], + stack: true + }]) + +The stacking order is determined by the order of the data series in the array +(later series end up on top of the previous). + +Internally, the plugin modifies the datapoints in each series, adding an +offset to the y value. For line series, extra data points are inserted through +interpolation. If there's a second y value, it's also adjusted (e.g for bar +charts or filled areas). + +*/ + +(function ($) { + var options = { + series: { stack: null } // or number/string + }; + + function init(plot) { + function findMatchingSeries(s, allseries) { + var res = null; + for (var i = 0; i < allseries.length; ++i) { + if (s === allseries[i]) break; + + if (allseries[i].stack === s.stack) { + res = allseries[i]; + } + } + + return res; + } + + function addBottomPoints (s, datapoints) { + var formattedPoints = []; + for (var i = 0; i < datapoints.points.length; i += 2) { + formattedPoints.push(datapoints.points[i]); + formattedPoints.push(datapoints.points[i + 1]); + formattedPoints.push(0); + } + + datapoints.format.push({ + x: false, + y: true, + number: true, + required: false, + computeRange: s.yaxis.options.autoScale !== 'none', + defaultValue: 0 + }); + datapoints.points = formattedPoints; + datapoints.pointsize = 3; + } + + function stackData(plot, s, datapoints) { + if (s.stack == null || s.stack === false) return; + + var needsBottom = s.bars.show || (s.lines.show && s.lines.fill); + var hasBottom = datapoints.pointsize > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y); + // Series data is missing bottom points - need to format + if (needsBottom && !hasBottom) { + addBottomPoints(s, datapoints); + } + + var other = findMatchingSeries(s, plot.getData()); + if (!other) return; + + var ps = datapoints.pointsize, + points = datapoints.points, + otherps = other.datapoints.pointsize, + otherpoints = other.datapoints.points, + newpoints = [], + px, py, intery, qx, qy, bottom, + withlines = s.lines.show, + horizontal = s.bars.horizontal, + withsteps = withlines && s.lines.steps, + fromgap = true, + keyOffset = horizontal ? 1 : 0, + accumulateOffset = horizontal ? 0 : 1, + i = 0, j = 0, l, m; + + while (true) { + if (i >= points.length) break; + + l = newpoints.length; + + if (points[i] == null) { + // copy gaps + for (m = 0; m < ps; ++m) { + newpoints.push(points[i + m]); + } + + i += ps; + } else if (j >= otherpoints.length) { + // for lines, we can't use the rest of the points + if (!withlines) { + for (m = 0; m < ps; ++m) { + newpoints.push(points[i + m]); + } + } + + i += ps; + } else if (otherpoints[j] == null) { + // oops, got a gap + for (m = 0; m < ps; ++m) { + newpoints.push(null); + } + + fromgap = true; + j += otherps; + } else { + // cases where we actually got two points + px = points[i + keyOffset]; + py = points[i + accumulateOffset]; + qx = otherpoints[j + keyOffset]; + qy = otherpoints[j + accumulateOffset]; + bottom = 0; + + if (px === qx) { + for (m = 0; m < ps; ++m) { + newpoints.push(points[i + m]); + } + + newpoints[l + accumulateOffset] += qy; + bottom = qy; + + i += ps; + j += otherps; + } else if (px > qx) { + // we got past point below, might need to + // insert interpolated extra point + if (withlines && i > 0 && points[i - ps] != null) { + intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); + newpoints.push(qx); + newpoints.push(intery + qy); + for (m = 2; m < ps; ++m) { + newpoints.push(points[i + m]); + } + + bottom = qy; + } + + j += otherps; + } else { // px < qx + if (fromgap && withlines) { + // if we come from a gap, we just skip this point + i += ps; + continue; + } + + for (m = 0; m < ps; ++m) { + newpoints.push(points[i + m]); + } + + // we might be able to interpolate a point below, + // this can give us a better y + if (withlines && j > 0 && otherpoints[j - otherps] != null) { + bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); + } + + newpoints[l + accumulateOffset] += bottom; + + i += ps; + } + + fromgap = false; + + if (l !== newpoints.length && needsBottom) { + newpoints[l + 2] += bottom; + } + } + + // maintain the line steps invariant + if (withsteps && l !== newpoints.length && l > 0 && + newpoints[l] !== null && + newpoints[l] !== newpoints[l - ps] && + newpoints[l + 1] !== newpoints[l - ps + 1]) { + for (m = 0; m < ps; ++m) { + newpoints[l + ps + m] = newpoints[l + m]; + } + + newpoints[l + 1] = newpoints[l - ps + 1]; + } + } + + datapoints.points = newpoints; + } + + plot.hooks.processDatapoints.push(stackData); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'stack', + version: '1.2' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.symbol.js b/frontend/lib/flot/jquery.flot.symbol.js new file mode 100644 index 0000000..0e06513 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.symbol.js @@ -0,0 +1,98 @@ +/* Flot plugin that adds some extra symbols for plotting points. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The symbols are accessed as strings through the standard symbol options: + + series: { + points: { + symbol: "square" // or "diamond", "triangle", "cross", "plus", "ellipse", "rectangle" + } + } + +*/ + +(function ($) { + // we normalize the area of each symbol so it is approximately the + // same as a circle of the given radius + + var square = function (ctx, x, y, radius, shadow) { + // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 + var size = radius * Math.sqrt(Math.PI) / 2; + ctx.rect(x - size, y - size, size + size, size + size); + }, + rectangle = function (ctx, x, y, radius, shadow) { + // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 + var size = radius * Math.sqrt(Math.PI) / 2; + ctx.rect(x - size, y - size, size + size, size + size); + }, + diamond = function (ctx, x, y, radius, shadow) { + // pi * r^2 = 2s^2 => s = r * sqrt(pi/2) + var size = radius * Math.sqrt(Math.PI / 2); + ctx.moveTo(x - size, y); + ctx.lineTo(x, y - size); + ctx.lineTo(x + size, y); + ctx.lineTo(x, y + size); + ctx.lineTo(x - size, y); + ctx.lineTo(x, y - size); + }, + triangle = function (ctx, x, y, radius, shadow) { + // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3)) + var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); + var height = size * Math.sin(Math.PI / 3); + ctx.moveTo(x - size / 2, y + height / 2); + ctx.lineTo(x + size / 2, y + height / 2); + if (!shadow) { + ctx.lineTo(x, y - height / 2); + ctx.lineTo(x - size / 2, y + height / 2); + ctx.lineTo(x + size / 2, y + height / 2); + } + }, + cross = function (ctx, x, y, radius, shadow) { + // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 + var size = radius * Math.sqrt(Math.PI) / 2; + ctx.moveTo(x - size, y - size); + ctx.lineTo(x + size, y + size); + ctx.moveTo(x - size, y + size); + ctx.lineTo(x + size, y - size); + }, + ellipse = function(ctx, x, y, radius, shadow, fill) { + if (!shadow) { + ctx.moveTo(x + radius, y); + ctx.arc(x, y, radius, 0, Math.PI * 2, false); + } + }, + plus = function (ctx, x, y, radius, shadow) { + var size = radius * Math.sqrt(Math.PI / 2); + ctx.moveTo(x - size, y); + ctx.lineTo(x + size, y); + ctx.moveTo(x, y + size); + ctx.lineTo(x, y - size); + }, + handlers = { + square: square, + rectangle: rectangle, + diamond: diamond, + triangle: triangle, + cross: cross, + ellipse: ellipse, + plus: plus + }; + + square.fill = true; + rectangle.fill = true; + diamond.fill = true; + triangle.fill = true; + ellipse.fill = true; + + function init(plot) { + plot.drawSymbol = handlers; + } + + $.plot.plugins.push({ + init: init, + name: 'symbols', + version: '1.0' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.threshold.js b/frontend/lib/flot/jquery.flot.threshold.js new file mode 100644 index 0000000..db5a59c --- /dev/null +++ b/frontend/lib/flot/jquery.flot.threshold.js @@ -0,0 +1,143 @@ +/* Flot plugin for thresholding data. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin supports these options: + + series: { + threshold: { + below: number + color: colorspec + } + } + +It can also be applied to a single series, like this: + + $.plot( $("#placeholder"), [{ + data: [ ... ], + threshold: { ... } + }]) + +An array can be passed for multiple thresholding, like this: + + threshold: [{ + below: number1 + color: color1 + },{ + below: number2 + color: color2 + }] + +These multiple threshold objects can be passed in any order since they are +sorted by the processing function. + +The data points below "below" are drawn with the specified color. This makes +it easy to mark points below 0, e.g. for budget data. + +Internally, the plugin works by splitting the data into two series, above and +below the threshold. The extra series below the threshold will have its label +cleared and the special "originSeries" attribute set to the original series. +You may need to check for this in hover events. + +*/ + +(function ($) { + var options = { + series: { threshold: null } // or { below: number, color: color spec} + }; + + function init(plot) { + function thresholdData(plot, s, datapoints, below, color) { + var ps = datapoints.pointsize, i, x, y, p, prevp, + thresholded = $.extend({}, s); // note: shallow copy + + thresholded.datapoints = { points: [], pointsize: ps, format: datapoints.format }; + thresholded.label = null; + thresholded.color = color; + thresholded.threshold = null; + thresholded.originSeries = s; + thresholded.data = []; + + var origpoints = datapoints.points, + addCrossingPoints = s.lines.show; + + var threspoints = []; + var newpoints = []; + var m; + + for (i = 0; i < origpoints.length; i += ps) { + x = origpoints[i]; + y = origpoints[i + 1]; + + prevp = p; + if (y < below) p = threspoints; + else p = newpoints; + + if (addCrossingPoints && prevp !== p && + x !== null && i > 0 && + origpoints[i - ps] != null) { + var interx = x + (below - y) * (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]); + prevp.push(interx); + prevp.push(below); + for (m = 2; m < ps; ++m) { + prevp.push(origpoints[i + m]); + } + + p.push(null); // start new segment + p.push(null); + for (m = 2; m < ps; ++m) { + p.push(origpoints[i + m]); + } + + p.push(interx); + p.push(below); + for (m = 2; m < ps; ++m) { + p.push(origpoints[i + m]); + } + } + + p.push(x); + p.push(y); + for (m = 2; m < ps; ++m) { + p.push(origpoints[i + m]); + } + } + + datapoints.points = newpoints; + thresholded.datapoints.points = threspoints; + + if (thresholded.datapoints.points.length > 0) { + var origIndex = $.inArray(s, plot.getData()); + // Insert newly-generated series right after original one (to prevent it from becoming top-most) + plot.getData().splice(origIndex + 1, 0, thresholded); + } + + // FIXME: there are probably some edge cases left in bars + } + + function processThresholds(plot, s, datapoints) { + if (!s.threshold) return; + if (s.threshold instanceof Array) { + s.threshold.sort(function(a, b) { + return a.below - b.below; + }); + + $(s.threshold).each(function(i, th) { + thresholdData(plot, s, datapoints, th.below, th.color); + }); + } else { + thresholdData(plot, s, datapoints, s.threshold.below, s.threshold.color); + } + } + + plot.hooks.processDatapoints.push(processThresholds); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'threshold', + version: '1.2' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.time.js b/frontend/lib/flot/jquery.flot.time.js new file mode 100644 index 0000000..012d658 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.time.js @@ -0,0 +1,585 @@ +/* Pretty handling of time axes. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +Set axis.mode to "time" to enable. See the section "Time series data" in +API.txt for details. +*/ + +(function($) { + 'use strict'; + + var options = { + xaxis: { + timezone: null, // "browser" for local to the client or timezone for timezone-js + timeformat: null, // format string to use + twelveHourClock: false, // 12 or 24 time in time mode + monthNames: null, // list of names of months + timeBase: 'seconds' // are the values in given in mircoseconds, milliseconds or seconds + }, + yaxis: { + timeBase: 'seconds' + } + }; + + var floorInBase = $.plot.saturated.floorInBase; + + // Method to provide microsecond support to Date like classes. + var CreateMicroSecondDate = function(dateType, microEpoch) { + var newDate = new dateType(microEpoch); + + var oldSetTime = newDate.setTime.bind(newDate); + newDate.update = function(microEpoch) { + oldSetTime(microEpoch); + + // Round epoch to 3 decimal accuracy + microEpoch = Math.round(microEpoch*1000)/1000; + + // Microseconds are stored as integers + this.microseconds = 1000 * (microEpoch - Math.floor(microEpoch)); + }; + + var oldGetTime = newDate.getTime.bind(newDate); + newDate.getTime = function () { + var microEpoch = oldGetTime() + this.microseconds / 1000; + return microEpoch; + }; + + newDate.setTime = function (microEpoch) { + this.update(microEpoch); + }; + + newDate.getMicroseconds = function() { + return this.microseconds; + }; + + newDate.setMicroseconds = function(microseconds) { + var epochWithoutMicroseconds = oldGetTime(); + var newEpoch = epochWithoutMicroseconds + microseconds/1000; + this.update(newEpoch); + }; + + newDate.setUTCMicroseconds = function(microseconds) { this.setMicroseconds(microseconds); } + + newDate.getUTCMicroseconds = function() { return this.getMicroseconds(); } + + newDate.microseconds = null; + newDate.microEpoch = null; + newDate.update(microEpoch); + return newDate; + } + + // Returns a string with the date d formatted according to fmt. + // A subset of the Open Group's strftime format is supported. + + function formatDate(d, fmt, monthNames, dayNames) { + if (typeof d.strftime === "function") { + return d.strftime(fmt); + } + + var leftPad = function(n, pad) { + n = "" + n; + pad = "" + (pad == null ? "0" : pad); + return n.length == 1 ? pad + n : n; + }; + + var formatSubSeconds = function(milliseconds, microseconds, numberDecimalPlaces) { + var totalMicroseconds = milliseconds * 1000 + microseconds; + var formattedString; + if (numberDecimalPlaces < 6 && numberDecimalPlaces > 0) { + var magnitude = parseFloat('1e' + (numberDecimalPlaces - 6)); + totalMicroseconds = Math.round(Math.round(totalMicroseconds * magnitude) / magnitude); + formattedString = ('00000' + totalMicroseconds).slice(-6,-(6 - numberDecimalPlaces)); + } else { + totalMicroseconds = Math.round(totalMicroseconds) + formattedString = ('00000' + totalMicroseconds).slice(-6); + } + return formattedString; + }; + + var r = []; + var escape = false; + var hours = d.getHours(); + var isAM = hours < 12; + + if (!monthNames) { + monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + } + + if (!dayNames) { + dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + } + + var hours12; + if (hours > 12) { + hours12 = hours - 12; + } else if (hours == 0) { + hours12 = 12; + } else { + hours12 = hours; + } + + var decimals = -1; + for (var i = 0; i < fmt.length; ++i) { + var c = fmt.charAt(i); + + if (!isNaN(Number(c)) && Number(c) > 0) { + decimals = Number(c); + } else if (escape) { + switch (c) { + case 'a': c = "" + dayNames[d.getDay()]; break; + case 'b': c = "" + monthNames[d.getMonth()]; break; + case 'd': c = leftPad(d.getDate()); break; + case 'e': c = leftPad(d.getDate(), " "); break; + case 'h': // For back-compat with 0.7; remove in 1.0 + case 'H': c = leftPad(hours); break; + case 'I': c = leftPad(hours12); break; + case 'l': c = leftPad(hours12, " "); break; + case 'm': c = leftPad(d.getMonth() + 1); break; + case 'M': c = leftPad(d.getMinutes()); break; + // quarters not in Open Group's strftime specification + case 'q': + c = "" + (Math.floor(d.getMonth() / 3) + 1); break; + case 'S': c = leftPad(d.getSeconds()); break; + case 's': c = "" + formatSubSeconds(d.getMilliseconds(), d.getMicroseconds(), decimals); break; + case 'y': c = leftPad(d.getFullYear() % 100); break; + case 'Y': c = "" + d.getFullYear(); break; + case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; + case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; + case 'w': c = "" + d.getDay(); break; + } + r.push(c); + escape = false; + } else { + if (c == "%") { + escape = true; + } else { + r.push(c); + } + } + } + + return r.join(""); + } + + // To have a consistent view of time-based data independent of which time + // zone the client happens to be in we need a date-like object independent + // of time zones. This is done through a wrapper that only calls the UTC + // versions of the accessor methods. + + function makeUtcWrapper(d) { + function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { + sourceObj[sourceMethod] = function() { + return targetObj[targetMethod].apply(targetObj, arguments); + }; + } + + var utc = { + date: d + }; + + // support strftime, if found + if (d.strftime !== undefined) { + addProxyMethod(utc, "strftime", d, "strftime"); + } + + addProxyMethod(utc, "getTime", d, "getTime"); + addProxyMethod(utc, "setTime", d, "setTime"); + + var props = ["Date", "Day", "FullYear", "Hours", "Minutes", "Month", "Seconds", "Milliseconds", "Microseconds"]; + + for (var p = 0; p < props.length; p++) { + addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); + addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); + } + + return utc; + } + + // select time zone strategy. This returns a date-like object tied to the + // desired timezone + function dateGenerator(ts, opts) { + var maxDateValue = 8640000000000000; + + if (opts && opts.timeBase === 'seconds') { + ts *= 1000; + } else if (opts.timeBase === 'microseconds') { + ts /= 1000; + } + + if (ts > maxDateValue) { + ts = maxDateValue; + } else if (ts < -maxDateValue) { + ts = -maxDateValue; + } + + if (opts.timezone === "browser") { + return CreateMicroSecondDate(Date, ts); + } else if (!opts.timezone || opts.timezone === "utc") { + return makeUtcWrapper(CreateMicroSecondDate(Date, ts)); + } else if (typeof timezoneJS !== "undefined" && typeof timezoneJS.Date !== "undefined") { + var d = CreateMicroSecondDate(timezoneJS.Date, ts); + // timezone-js is fickle, so be sure to set the time zone before + // setting the time. + d.setTimezone(opts.timezone); + d.setTime(ts); + return d; + } else { + return makeUtcWrapper(CreateMicroSecondDate(Date, ts)); + } + } + + // map of app. size of time units in seconds + var timeUnitSizeSeconds = { + "microsecond": 0.000001, + "millisecond": 0.001, + "second": 1, + "minute": 60, + "hour": 60 * 60, + "day": 24 * 60 * 60, + "month": 30 * 24 * 60 * 60, + "quarter": 3 * 30 * 24 * 60 * 60, + "year": 365.2425 * 24 * 60 * 60 + }; + + // map of app. size of time units in milliseconds + var timeUnitSizeMilliseconds = { + "microsecond": 0.001, + "millisecond": 1, + "second": 1000, + "minute": 60 * 1000, + "hour": 60 * 60 * 1000, + "day": 24 * 60 * 60 * 1000, + "month": 30 * 24 * 60 * 60 * 1000, + "quarter": 3 * 30 * 24 * 60 * 60 * 1000, + "year": 365.2425 * 24 * 60 * 60 * 1000 + }; + + // map of app. size of time units in microseconds + var timeUnitSizeMicroseconds = { + "microsecond": 1, + "millisecond": 1000, + "second": 1000000, + "minute": 60 * 1000000, + "hour": 60 * 60 * 1000000, + "day": 24 * 60 * 60 * 1000000, + "month": 30 * 24 * 60 * 60 * 1000000, + "quarter": 3 * 30 * 24 * 60 * 60 * 1000000, + "year": 365.2425 * 24 * 60 * 60 * 1000000 + }; + + // the allowed tick sizes, after 1 year we use + // an integer algorithm + + var baseSpec = [ + [1, "microsecond"], [2, "microsecond"], [5, "microsecond"], [10, "microsecond"], + [25, "microsecond"], [50, "microsecond"], [100, "microsecond"], [250, "microsecond"], [500, "microsecond"], + [1, "millisecond"], [2, "millisecond"], [5, "millisecond"], [10, "millisecond"], + [25, "millisecond"], [50, "millisecond"], [100, "millisecond"], [250, "millisecond"], [500, "millisecond"], + [1, "second"], [2, "second"], [5, "second"], [10, "second"], + [30, "second"], + [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], + [30, "minute"], + [1, "hour"], [2, "hour"], [4, "hour"], + [8, "hour"], [12, "hour"], + [1, "day"], [2, "day"], [3, "day"], + [0.25, "month"], [0.5, "month"], [1, "month"], + [2, "month"] + ]; + + // we don't know which variant(s) we'll need yet, but generating both is + // cheap + + var specMonths = baseSpec.concat([[3, "month"], [6, "month"], + [1, "year"]]); + var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], + [1, "year"]]); + + + function dateTickGenerator(axis) { + var opts = axis.options, + ticks = [], + d = dateGenerator(axis.min, opts), + minSize = 0; + + // make quarter use a possibility if quarters are + // mentioned in either of these options + var spec = (opts.tickSize && opts.tickSize[1] === + "quarter") || + (opts.minTickSize && opts.minTickSize[1] === + "quarter") ? specQuarters : specMonths; + + var timeUnitSize; + if (opts.timeBase === 'seconds') { + timeUnitSize = timeUnitSizeSeconds; + } else if (opts.timeBase === 'microseconds') { + timeUnitSize = timeUnitSizeMicroseconds; + } else { + timeUnitSize = timeUnitSizeMilliseconds; + } + + if (opts.minTickSize !== null && opts.minTickSize !== undefined) { + if (typeof opts.tickSize === "number") { + minSize = opts.tickSize; + } else { + minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; + } + } + + for (var i = 0; i < spec.length - 1; ++i) { + if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] + + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 && + spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { + break; + } + } + + var size = spec[i][0]; + var unit = spec[i][1]; + // special-case the possibility of several years + if (unit === "year") { + // if given a minTickSize in years, just use it, + // ensuring that it's an integer + + if (opts.minTickSize !== null && opts.minTickSize !== undefined && opts.minTickSize[1] === "year") { + size = Math.floor(opts.minTickSize[0]); + } else { + var magn = parseFloat('1e' + Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); + var norm = (axis.delta / timeUnitSize.year) / magn; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + } + + // minimum size for years is 1 + + if (size < 1) { + size = 1; + } + } + + axis.tickSize = opts.tickSize || [size, unit]; + var tickSize = axis.tickSize[0]; + unit = axis.tickSize[1]; + + var step = tickSize * timeUnitSize[unit]; + + if (unit === "microsecond") { + d.setMicroseconds(floorInBase(d.getMicroseconds(), tickSize)); + } else if (unit === "millisecond") { + d.setMilliseconds(floorInBase(d.getMilliseconds(), tickSize)); + } else if (unit === "second") { + d.setSeconds(floorInBase(d.getSeconds(), tickSize)); + } else if (unit === "minute") { + d.setMinutes(floorInBase(d.getMinutes(), tickSize)); + } else if (unit === "hour") { + d.setHours(floorInBase(d.getHours(), tickSize)); + } else if (unit === "month") { + d.setMonth(floorInBase(d.getMonth(), tickSize)); + } else if (unit === "quarter") { + d.setMonth(3 * floorInBase(d.getMonth() / 3, + tickSize)); + } else if (unit === "year") { + d.setFullYear(floorInBase(d.getFullYear(), tickSize)); + } + + // reset smaller components + + if (step >= timeUnitSize.millisecond) { + if (step >= timeUnitSize.second) { + d.setMicroseconds(0); + } else { + d.setMicroseconds(d.getMilliseconds()*1000); + } + } + if (step >= timeUnitSize.minute) { + d.setSeconds(0); + } + if (step >= timeUnitSize.hour) { + d.setMinutes(0); + } + if (step >= timeUnitSize.day) { + d.setHours(0); + } + if (step >= timeUnitSize.day * 4) { + d.setDate(1); + } + if (step >= timeUnitSize.month * 2) { + d.setMonth(floorInBase(d.getMonth(), 3)); + } + if (step >= timeUnitSize.quarter * 2) { + d.setMonth(floorInBase(d.getMonth(), 6)); + } + if (step >= timeUnitSize.year) { + d.setMonth(0); + } + + var carry = 0; + var v = Number.NaN; + var v1000; + var prev; + do { + prev = v; + v1000 = d.getTime(); + if (opts && opts.timeBase === 'seconds') { + v = v1000 / 1000; + } else if (opts && opts.timeBase === 'microseconds') { + v = v1000 * 1000; + } else { + v = v1000; + } + + ticks.push(v); + + if (unit === "month" || unit === "quarter") { + if (tickSize < 1) { + // a bit complicated - we'll divide the + // month/quarter up but we need to take + // care of fractions so we don't end up in + // the middle of a day + d.setDate(1); + var start = d.getTime(); + d.setMonth(d.getMonth() + + (unit === "quarter" ? 3 : 1)); + var end = d.getTime(); + d.setTime((v + carry * timeUnitSize.hour + (end - start) * tickSize)); + carry = d.getHours(); + d.setHours(0); + } else { + d.setMonth(d.getMonth() + + tickSize * (unit === "quarter" ? 3 : 1)); + } + } else if (unit === "year") { + d.setFullYear(d.getFullYear() + tickSize); + } else { + if (opts.timeBase === 'seconds') { + d.setTime((v + step) * 1000); + } else if (opts.timeBase === 'microseconds') { + d.setTime((v + step) / 1000); + } else { + d.setTime(v + step); + } + } + } while (v < axis.max && v !== prev); + + return ticks; + }; + + function init(plot) { + plot.hooks.processOptions.push(function (plot) { + $.each(plot.getAxes(), function(axisName, axis) { + var opts = axis.options; + if (opts.mode === "time") { + axis.tickGenerator = dateTickGenerator; + + axis.tickFormatter = function (v, axis) { + var d = dateGenerator(v, axis.options); + + // first check global format + if (opts.timeformat != null) { + return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); + } + + // possibly use quarters if quarters are mentioned in + // any of these places + var useQuarters = (axis.options.tickSize && + axis.options.tickSize[1] == "quarter") || + (axis.options.minTickSize && + axis.options.minTickSize[1] == "quarter"); + + var timeUnitSize; + if (opts.timeBase === 'seconds') { + timeUnitSize = timeUnitSizeSeconds; + } else if (opts.timeBase === 'microseconds') { + timeUnitSize = timeUnitSizeMicroseconds; + } else { + timeUnitSize = timeUnitSizeMilliseconds; + } + + var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; + var span = axis.max - axis.min; + var suffix = (opts.twelveHourClock) ? " %p" : ""; + var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; + var factor; + var fmt; + + if (opts.timeBase === 'seconds') { + factor = 1; + } else if (opts.timeBase === 'microseconds') { + factor = 1000000 + } else { + factor = 1000; + } + + if (t < timeUnitSize.second) { + var decimals = -Math.floor(Math.log10(t/factor)) + + // the two-and-halves require an additional decimal + if (String(t).indexOf('25') > -1) { + decimals++; + } + + fmt = "%S.%" + decimals + "s"; + } else + if (t < timeUnitSize.minute) { + fmt = hourCode + ":%M:%S" + suffix; + } else if (t < timeUnitSize.day) { + if (span < 2 * timeUnitSize.day) { + fmt = hourCode + ":%M" + suffix; + } else { + fmt = "%b %d " + hourCode + ":%M" + suffix; + } + } else if (t < timeUnitSize.month) { + fmt = "%b %d"; + } else if ((useQuarters && t < timeUnitSize.quarter) || + (!useQuarters && t < timeUnitSize.year)) { + if (span < timeUnitSize.year) { + fmt = "%b"; + } else { + fmt = "%b %Y"; + } + } else if (useQuarters && t < timeUnitSize.year) { + if (span < timeUnitSize.year) { + fmt = "Q%q"; + } else { + fmt = "Q%q %Y"; + } + } else { + fmt = "%Y"; + } + + var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); + + return rt; + }; + } + }); + }); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'time', + version: '1.0' + }); + + // Time-axis support used to be in Flot core, which exposed the + // formatDate function on the plot object. Various plugins depend + // on the function, so we need to re-expose it here. + + $.plot.formatDate = formatDate; + $.plot.dateGenerator = dateGenerator; + $.plot.dateTickGenerator = dateTickGenerator; + $.plot.makeUtcWrapper = makeUtcWrapper; +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.touch.js b/frontend/lib/flot/jquery.flot.touch.js new file mode 100644 index 0000000..492cf68 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.touch.js @@ -0,0 +1,320 @@ + +/* global jQuery */ + +(function($) { + 'use strict'; + + var options = { + propagateSupportedGesture: false + }; + + function init(plot) { + plot.hooks.processOptions.push(initTouchNavigation); + } + + function initTouchNavigation(plot, options) { + var gestureState = { + twoTouches: false, + currentTapStart: { x: 0, y: 0 }, + currentTapEnd: { x: 0, y: 0 }, + prevTap: { x: 0, y: 0 }, + currentTap: { x: 0, y: 0 }, + interceptedLongTap: false, + isUnsupportedGesture: false, + prevTapTime: null, + tapStartTime: null, + longTapTriggerId: null + }, + maxDistanceBetweenTaps = 20, + maxIntervalBetweenTaps = 500, + maxLongTapDistance = 20, + minLongTapDuration = 1500, + pressedTapDuration = 125, + mainEventHolder; + + function interpretGestures(e) { + var o = plot.getOptions(); + + if (!o.pan.active && !o.zoom.active) { + return; + } + + updateOnMultipleTouches(e); + mainEventHolder.dispatchEvent(new CustomEvent('touchevent', { detail: e })); + + if (isPinchEvent(e)) { + executeAction(e, 'pinch'); + } else { + executeAction(e, 'pan'); + if (!wasPinchEvent(e)) { + if (isDoubleTap(e)) { + executeAction(e, 'doubleTap'); + } + executeAction(e, 'tap'); + executeAction(e, 'longTap'); + } + } + } + + function executeAction(e, gesture) { + switch (gesture) { + case 'pan': + pan[e.type](e); + break; + case 'pinch': + pinch[e.type](e); + break; + case 'doubleTap': + doubleTap.onDoubleTap(e); + break; + case 'longTap': + longTap[e.type](e); + break; + case 'tap': + tap[e.type](e); + break; + } + } + + function bindEvents(plot, eventHolder) { + mainEventHolder = eventHolder[0]; + eventHolder[0].addEventListener('touchstart', interpretGestures, false); + eventHolder[0].addEventListener('touchmove', interpretGestures, false); + eventHolder[0].addEventListener('touchend', interpretGestures, false); + } + + function shutdown(plot, eventHolder) { + eventHolder[0].removeEventListener('touchstart', interpretGestures); + eventHolder[0].removeEventListener('touchmove', interpretGestures); + eventHolder[0].removeEventListener('touchend', interpretGestures); + if (gestureState.longTapTriggerId) { + clearTimeout(gestureState.longTapTriggerId); + gestureState.longTapTriggerId = null; + } + } + + var pan = { + touchstart: function(e) { + updatePrevForDoubleTap(); + updateCurrentForDoubleTap(e); + updateStateForLongTapStart(e); + + mainEventHolder.dispatchEvent(new CustomEvent('panstart', { detail: e })); + }, + + touchmove: function(e) { + preventEventBehaviors(e); + + updateCurrentForDoubleTap(e); + updateStateForLongTapEnd(e); + + if (!gestureState.isUnsupportedGesture) { + mainEventHolder.dispatchEvent(new CustomEvent('pandrag', { detail: e })); + } + }, + + touchend: function(e) { + preventEventBehaviors(e); + + if (wasPinchEvent(e)) { + mainEventHolder.dispatchEvent(new CustomEvent('pinchend', { detail: e })); + mainEventHolder.dispatchEvent(new CustomEvent('panstart', { detail: e })); + } else if (noTouchActive(e)) { + mainEventHolder.dispatchEvent(new CustomEvent('panend', { detail: e })); + } + } + }; + + var pinch = { + touchstart: function(e) { + mainEventHolder.dispatchEvent(new CustomEvent('pinchstart', { detail: e })); + }, + + touchmove: function(e) { + preventEventBehaviors(e); + gestureState.twoTouches = isPinchEvent(e); + if (!gestureState.isUnsupportedGesture) { + mainEventHolder.dispatchEvent(new CustomEvent('pinchdrag', { detail: e })); + } + }, + + touchend: function(e) { + preventEventBehaviors(e); + } + }; + + var doubleTap = { + onDoubleTap: function(e) { + preventEventBehaviors(e); + mainEventHolder.dispatchEvent(new CustomEvent('doubletap', { detail: e })); + } + }; + + var longTap = { + touchstart: function(e) { + longTap.waitForLongTap(e); + }, + + touchmove: function(e) { + }, + + touchend: function(e) { + if (gestureState.longTapTriggerId) { + clearTimeout(gestureState.longTapTriggerId); + gestureState.longTapTriggerId = null; + } + }, + + isLongTap: function(e) { + var currentTime = new Date().getTime(), + tapDuration = currentTime - gestureState.tapStartTime; + if (tapDuration >= minLongTapDuration && !gestureState.interceptedLongTap) { + if (distance(gestureState.currentTapStart.x, gestureState.currentTapStart.y, gestureState.currentTapEnd.x, gestureState.currentTapEnd.y) < maxLongTapDistance) { + gestureState.interceptedLongTap = true; + return true; + } + } + return false; + }, + + waitForLongTap: function(e) { + var longTapTrigger = function() { + if (longTap.isLongTap(e)) { + mainEventHolder.dispatchEvent(new CustomEvent('longtap', { detail: e })); + } + gestureState.longTapTriggerId = null; + }; + if (!gestureState.longTapTriggerId) { + gestureState.longTapTriggerId = setTimeout(longTapTrigger, minLongTapDuration); + } + } + }; + + var tap = { + touchstart: function(e) { + gestureState.tapStartTime = new Date().getTime(); + }, + + touchmove: function(e) { + }, + + touchend: function(e) { + if (tap.isTap(e)) { + mainEventHolder.dispatchEvent(new CustomEvent('tap', { detail: e })); + preventEventBehaviors(e); + } + }, + + isTap: function(e) { + var currentTime = new Date().getTime(), + tapDuration = currentTime - gestureState.tapStartTime; + if (tapDuration <= pressedTapDuration) { + if (distance(gestureState.currentTapStart.x, gestureState.currentTapStart.y, gestureState.currentTapEnd.x, gestureState.currentTapEnd.y) < maxLongTapDistance) { + return true; + } + } + return false; + } + }; + + if (options.pan.enableTouch === true || options.zoom.enableTouch) { + plot.hooks.bindEvents.push(bindEvents); + plot.hooks.shutdown.push(shutdown); + }; + + function updatePrevForDoubleTap() { + gestureState.prevTap = { + x: gestureState.currentTap.x, + y: gestureState.currentTap.y + }; + }; + + function updateCurrentForDoubleTap(e) { + gestureState.currentTap = { + x: e.touches[0].pageX, + y: e.touches[0].pageY + }; + } + + function updateStateForLongTapStart(e) { + gestureState.tapStartTime = new Date().getTime(); + gestureState.interceptedLongTap = false; + gestureState.currentTapStart = { + x: e.touches[0].pageX, + y: e.touches[0].pageY + }; + gestureState.currentTapEnd = { + x: e.touches[0].pageX, + y: e.touches[0].pageY + }; + }; + + function updateStateForLongTapEnd(e) { + gestureState.currentTapEnd = { + x: e.touches[0].pageX, + y: e.touches[0].pageY + }; + }; + + function isDoubleTap(e) { + var currentTime = new Date().getTime(), + intervalBetweenTaps = currentTime - gestureState.prevTapTime; + + if (intervalBetweenTaps >= 0 && intervalBetweenTaps < maxIntervalBetweenTaps) { + if (distance(gestureState.prevTap.x, gestureState.prevTap.y, gestureState.currentTap.x, gestureState.currentTap.y) < maxDistanceBetweenTaps) { + e.firstTouch = gestureState.prevTap; + e.secondTouch = gestureState.currentTap; + return true; + } + } + gestureState.prevTapTime = currentTime; + return false; + } + + function preventEventBehaviors(e) { + if (!gestureState.isUnsupportedGesture) { + e.preventDefault(); + if (!plot.getOptions().propagateSupportedGesture) { + e.stopPropagation(); + } + } + } + + function distance(x1, y1, x2, y2) { + return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); + } + + function noTouchActive(e) { + return (e.touches && e.touches.length === 0); + } + + function wasPinchEvent(e) { + return (gestureState.twoTouches && e.touches.length === 1); + } + + function updateOnMultipleTouches(e) { + if (e.touches.length >= 3) { + gestureState.isUnsupportedGesture = true; + } else { + gestureState.isUnsupportedGesture = false; + } + } + + function isPinchEvent(e) { + if (e.touches && e.touches.length >= 2) { + if (e.touches[0].target === plot.getEventHolder() && + e.touches[1].target === plot.getEventHolder()) { + return true; + } + } + return false; + } + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'navigateTouch', + version: '0.3' + }); +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.touchNavigate.js b/frontend/lib/flot/jquery.flot.touchNavigate.js new file mode 100644 index 0000000..1cc5ea7 --- /dev/null +++ b/frontend/lib/flot/jquery.flot.touchNavigate.js @@ -0,0 +1,360 @@ +/* global jQuery */ + +(function($) { + 'use strict'; + + var options = { + zoom: { + enableTouch: false + }, + pan: { + enableTouch: false, + touchMode: 'manual' + }, + recenter: { + enableTouch: true + } + }; + + var ZOOM_DISTANCE_MARGIN = $.plot.uiConstants.ZOOM_DISTANCE_MARGIN; + + function init(plot) { + plot.hooks.processOptions.push(initTouchNavigation); + } + + function initTouchNavigation(plot, options) { + var gestureState = { + zoomEnable: false, + prevDistance: null, + prevTapTime: 0, + prevPanPosition: { x: 0, y: 0 }, + prevTapPosition: { x: 0, y: 0 } + }, + navigationState = { + prevTouchedAxis: 'none', + currentTouchedAxis: 'none', + touchedAxis: null, + navigationConstraint: 'unconstrained', + initialState: null, + }, + useManualPan = options.pan.interactive && options.pan.touchMode === 'manual', + smartPanLock = options.pan.touchMode === 'smartLock', + useSmartPan = options.pan.interactive && (smartPanLock || options.pan.touchMode === 'smart'), + pan, pinch, doubleTap; + + function bindEvents(plot, eventHolder) { + var o = plot.getOptions(); + + if (o.zoom.interactive && o.zoom.enableTouch) { + eventHolder[0].addEventListener('pinchstart', pinch.start, false); + eventHolder[0].addEventListener('pinchdrag', pinch.drag, false); + eventHolder[0].addEventListener('pinchend', pinch.end, false); + } + + if (o.pan.interactive && o.pan.enableTouch) { + eventHolder[0].addEventListener('panstart', pan.start, false); + eventHolder[0].addEventListener('pandrag', pan.drag, false); + eventHolder[0].addEventListener('panend', pan.end, false); + } + + if ((o.recenter.interactive && o.recenter.enableTouch)) { + eventHolder[0].addEventListener('doubletap', doubleTap.recenterPlot, false); + } + } + + function shutdown(plot, eventHolder) { + eventHolder[0].removeEventListener('panstart', pan.start); + eventHolder[0].removeEventListener('pandrag', pan.drag); + eventHolder[0].removeEventListener('panend', pan.end); + eventHolder[0].removeEventListener('pinchstart', pinch.start); + eventHolder[0].removeEventListener('pinchdrag', pinch.drag); + eventHolder[0].removeEventListener('pinchend', pinch.end); + eventHolder[0].removeEventListener('doubletap', doubleTap.recenterPlot); + } + + pan = { + start: function(e) { + presetNavigationState(e, 'pan', gestureState); + updateData(e, 'pan', gestureState, navigationState); + + if (useSmartPan) { + var point = getPoint(e, 'pan'); + navigationState.initialState = plot.navigationState(point.x, point.y); + } + }, + + drag: function(e) { + presetNavigationState(e, 'pan', gestureState); + + if (useSmartPan) { + var point = getPoint(e, 'pan'); + plot.smartPan({ + x: navigationState.initialState.startPageX - point.x, + y: navigationState.initialState.startPageY - point.y + }, navigationState.initialState, navigationState.touchedAxis, false, smartPanLock); + } else if (useManualPan) { + plot.pan({ + left: -delta(e, 'pan', gestureState).x, + top: -delta(e, 'pan', gestureState).y, + axes: navigationState.touchedAxis + }); + updatePrevPanPosition(e, 'pan', gestureState, navigationState); + } + }, + + end: function(e) { + presetNavigationState(e, 'pan', gestureState); + + if (useSmartPan) { + plot.smartPan.end(); + } + + if (wasPinchEvent(e, gestureState)) { + updateprevPanPosition(e, 'pan', gestureState, navigationState); + } + } + }; + + var pinchDragTimeout; + pinch = { + start: function(e) { + if (pinchDragTimeout) { + clearTimeout(pinchDragTimeout); + pinchDragTimeout = null; + } + presetNavigationState(e, 'pinch', gestureState); + setPrevDistance(e, gestureState); + updateData(e, 'pinch', gestureState, navigationState); + }, + + drag: function(e) { + if (pinchDragTimeout) { + return; + } + pinchDragTimeout = setTimeout(function() { + presetNavigationState(e, 'pinch', gestureState); + plot.pan({ + left: -delta(e, 'pinch', gestureState).x, + top: -delta(e, 'pinch', gestureState).y, + axes: navigationState.touchedAxis + }); + updatePrevPanPosition(e, 'pinch', gestureState, navigationState); + + var dist = pinchDistance(e); + + if (gestureState.zoomEnable || Math.abs(dist - gestureState.prevDistance) > ZOOM_DISTANCE_MARGIN) { + zoomPlot(plot, e, gestureState, navigationState); + + //activate zoom mode + gestureState.zoomEnable = true; + } + pinchDragTimeout = null; + }, 1000 / 60); + }, + + end: function(e) { + if (pinchDragTimeout) { + clearTimeout(pinchDragTimeout); + pinchDragTimeout = null; + } + presetNavigationState(e, 'pinch', gestureState); + gestureState.prevDistance = null; + } + }; + + doubleTap = { + recenterPlot: function(e) { + if (e && e.detail && e.detail.type === 'touchstart') { + // only do not recenter for touch start; + recenterPlotOnDoubleTap(plot, e, gestureState, navigationState); + } + } + }; + + if (options.pan.enableTouch === true || options.zoom.enableTouch === true) { + plot.hooks.bindEvents.push(bindEvents); + plot.hooks.shutdown.push(shutdown); + } + + function presetNavigationState(e, gesture, gestureState) { + navigationState.touchedAxis = getAxis(plot, e, gesture, navigationState); + if (noAxisTouched(navigationState)) { + navigationState.navigationConstraint = 'unconstrained'; + } else { + navigationState.navigationConstraint = 'axisConstrained'; + } + } + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'navigateTouch', + version: '0.3' + }); + + function recenterPlotOnDoubleTap(plot, e, gestureState, navigationState) { + checkAxesForDoubleTap(plot, e, navigationState); + if ((navigationState.currentTouchedAxis === 'x' && navigationState.prevTouchedAxis === 'x') || + (navigationState.currentTouchedAxis === 'y' && navigationState.prevTouchedAxis === 'y') || + (navigationState.currentTouchedAxis === 'none' && navigationState.prevTouchedAxis === 'none')) { + var event; + + plot.recenter({ axes: navigationState.touchedAxis }); + + if (navigationState.touchedAxis) { + event = new $.Event('re-center', { detail: { axisTouched: navigationState.touchedAxis } }); + } else { + event = new $.Event('re-center', { detail: e }); + } + plot.getPlaceholder().trigger(event); + } + } + + function checkAxesForDoubleTap(plot, e, navigationState) { + var axis = plot.getTouchedAxis(e.detail.firstTouch.x, e.detail.firstTouch.y); + if (axis[0] !== undefined) { + navigationState.prevTouchedAxis = axis[0].direction; + } + + axis = plot.getTouchedAxis(e.detail.secondTouch.x, e.detail.secondTouch.y); + if (axis[0] !== undefined) { + navigationState.touchedAxis = axis; + navigationState.currentTouchedAxis = axis[0].direction; + } + + if (noAxisTouched(navigationState)) { + navigationState.touchedAxis = null; + navigationState.prevTouchedAxis = 'none'; + navigationState.currentTouchedAxis = 'none'; + } + } + + function zoomPlot(plot, e, gestureState, navigationState) { + var offset = plot.offset(), + center = { + left: 0, + top: 0 + }, + zoomAmount = pinchDistance(e) / gestureState.prevDistance, + dist = pinchDistance(e); + + center.left = getPoint(e, 'pinch').x - offset.left; + center.top = getPoint(e, 'pinch').y - offset.top; + + // send the computed touched axis to the zoom function so that it only zooms on that one + plot.zoom({ + center: center, + amount: zoomAmount, + axes: navigationState.touchedAxis + }); + gestureState.prevDistance = dist; + } + + function wasPinchEvent(e, gestureState) { + return (gestureState.zoomEnable && e.detail.touches.length === 1); + } + + function getAxis(plot, e, gesture, navigationState) { + if (e.type === 'pinchstart') { + var axisTouch1 = plot.getTouchedAxis(e.detail.touches[0].pageX, e.detail.touches[0].pageY); + var axisTouch2 = plot.getTouchedAxis(e.detail.touches[1].pageX, e.detail.touches[1].pageY); + + if (axisTouch1.length === axisTouch2.length && axisTouch1.toString() === axisTouch2.toString()) { + return axisTouch1; + } + } else if (e.type === 'panstart') { + return plot.getTouchedAxis(e.detail.touches[0].pageX, e.detail.touches[0].pageY); + } else if (e.type === 'pinchend') { + //update axis since instead on pinch, a pan event is made + return plot.getTouchedAxis(e.detail.touches[0].pageX, e.detail.touches[0].pageY); + } else { + return navigationState.touchedAxis; + } + } + + function noAxisTouched(navigationState) { + return (!navigationState.touchedAxis || navigationState.touchedAxis.length === 0); + } + + function setPrevDistance(e, gestureState) { + gestureState.prevDistance = pinchDistance(e); + } + + function updateData(e, gesture, gestureState, navigationState) { + var axisDir, + point = getPoint(e, gesture); + + switch (navigationState.navigationConstraint) { + case 'unconstrained': + navigationState.touchedAxis = null; + gestureState.prevTapPosition = { + x: gestureState.prevPanPosition.x, + y: gestureState.prevPanPosition.y + }; + gestureState.prevPanPosition = { + x: point.x, + y: point.y + }; + break; + case 'axisConstrained': + axisDir = navigationState.touchedAxis[0].direction; + navigationState.currentTouchedAxis = axisDir; + gestureState.prevTapPosition[axisDir] = gestureState.prevPanPosition[axisDir]; + gestureState.prevPanPosition[axisDir] = point[axisDir]; + break; + default: + break; + } + } + + function distance(x1, y1, x2, y2) { + return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); + } + + function pinchDistance(e) { + var t1 = e.detail.touches[0], + t2 = e.detail.touches[1]; + return distance(t1.pageX, t1.pageY, t2.pageX, t2.pageY); + } + + function updatePrevPanPosition(e, gesture, gestureState, navigationState) { + var point = getPoint(e, gesture); + + switch (navigationState.navigationConstraint) { + case 'unconstrained': + gestureState.prevPanPosition.x = point.x; + gestureState.prevPanPosition.y = point.y; + break; + case 'axisConstrained': + gestureState.prevPanPosition[navigationState.currentTouchedAxis] = + point[navigationState.currentTouchedAxis]; + break; + default: + break; + } + } + + function delta(e, gesture, gestureState) { + var point = getPoint(e, gesture); + + return { + x: point.x - gestureState.prevPanPosition.x, + y: point.y - gestureState.prevPanPosition.y + } + } + + function getPoint(e, gesture) { + if (gesture === 'pinch') { + return { + x: (e.detail.touches[0].pageX + e.detail.touches[1].pageX) / 2, + y: (e.detail.touches[0].pageY + e.detail.touches[1].pageY) / 2 + } + } else { + return { + x: e.detail.touches[0].pageX, + y: e.detail.touches[0].pageY + } + } + } +})(jQuery); diff --git a/frontend/lib/flot/jquery.flot.uiConstants.js b/frontend/lib/flot/jquery.flot.uiConstants.js new file mode 100644 index 0000000..627847d --- /dev/null +++ b/frontend/lib/flot/jquery.flot.uiConstants.js @@ -0,0 +1,10 @@ +(function ($) { + 'use strict'; + $.plot.uiConstants = { + SNAPPING_CONSTANT: 20, + PANHINT_LENGTH_CONSTANT: 10, + MINOR_TICKS_COUNT_CONSTANT: 4, + TICK_LENGTH_CONSTANT: 10, + ZOOM_DISTANCE_MARGIN: 25 + }; +})(jQuery); diff --git a/frontend/lib/flot/jquery.js b/frontend/lib/flot/jquery.js new file mode 100644 index 0000000..f9f969a --- /dev/null +++ b/frontend/lib/flot/jquery.js @@ -0,0 +1,9473 @@ +/*! + * jQuery JavaScript Library v1.8.3 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright 2012 jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: Tue Nov 13 2012 08:20:33 GMT-0500 (Eastern Standard Time) + */ +/* eslint-disable */ +(function( window, undefined ) { +var + // A central reference to the root jQuery(document) + rootjQuery, + + // The deferred used on DOM ready + readyList, + + // Use the correct document accordingly with window argument (sandbox) + document = window.document, + location = window.location, + navigator = window.navigator, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // Save a reference to some core methods + core_push = Array.prototype.push, + core_slice = Array.prototype.slice, + core_indexOf = Array.prototype.indexOf, + core_toString = Object.prototype.toString, + core_hasOwn = Object.prototype.hasOwnProperty, + core_trim = String.prototype.trim, + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Used for matching numbers + core_pnum = /[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source, + + // Used for detecting and trimming whitespace + core_rnotwhite = /\S/, + core_rspace = /\s+/, + + // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE) + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + rquickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, + rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return ( letter + "" ).toUpperCase(); + }, + + // The ready event handler and self cleanup method + DOMContentLoaded = function() { + if ( document.addEventListener ) { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + } else if ( document.readyState === "complete" ) { + // we're here because readyState === "complete" in oldIE + // which is good enough for us to call the dom ready! + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }, + + // [[Class]] -> type pairs + class2type = {}; + +jQuery.fn = jQuery.prototype = { + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem, ret, doc; + + // Handle $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + doc = ( context && context.nodeType ? context.ownerDocument || context : document ); + + // scripts is true for back-compat + selector = jQuery.parseHTML( match[1], doc, true ); + if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { + this.attr.call( selector, context, true ); + } + + return jQuery.merge( this, selector ); + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.8.3", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return core_slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) { + ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; + } else if ( name ) { + ret.selector = this.selector + "." + name + "(" + selector + ")"; + } + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Add the callback + jQuery.ready.promise().done( fn ); + + return this; + }, + + eq: function( i ) { + i = +i; + return i === -1 ? + this.slice( i ) : + this.slice( i, i + 1 ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + slice: function() { + return this.pushStack( core_slice.apply( this, arguments ), + "slice", core_slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: core_push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 1 ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger("ready").off("ready"); + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + isWindow: function( obj ) { + return obj != null && obj == obj.window; + }, + + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); + }, + + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ core_toString.call(obj) ] || "object"; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !core_hasOwn.call(obj, "constructor") && + !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || core_hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + var name; + for ( name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw new Error( msg ); + }, + + // data: string of html + // context (optional): If specified, the fragment will be created in this context, defaults to document + // scripts (optional): If true, will include scripts passed in the html string + parseHTML: function( data, context, scripts ) { + var parsed; + if ( !data || typeof data !== "string" ) { + return null; + } + if ( typeof context === "boolean" ) { + scripts = context; + context = 0; + } + context = context || document; + + // Single tag + if ( (parsed = rsingleTag.exec( data )) ) { + return [ context.createElement( parsed[1] ) ]; + } + + parsed = jQuery.buildFragment( [ data ], context, scripts ? null : [] ); + return jQuery.merge( [], + (parsed.cacheable ? jQuery.clone( parsed.fragment ) : parsed.fragment).childNodes ); + }, + + parseJSON: function( data ) { + if ( !data || typeof data !== "string") { + return null; + } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + + } + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + var xml, tmp; + if ( !data || typeof data !== "string" ) { + return null; + } + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; + }, + + noop: function() {}, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && core_rnotwhite.test( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + // args is for internal usage only + each: function( obj, callback, args ) { + var name, + i = 0, + length = obj.length, + isObj = length === undefined || jQuery.isFunction( obj ); + + if ( args ) { + if ( isObj ) { + for ( name in obj ) { + if ( callback.apply( obj[ name ], args ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.apply( obj[ i++ ], args ) === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in obj ) { + if ( callback.call( obj[ name ], name, obj[ name ] ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.call( obj[ i ], i, obj[ i++ ] ) === false ) { + break; + } + } + } + } + + return obj; + }, + + // Use native String.trim function wherever possible + trim: core_trim && !core_trim.call("\uFEFF\xA0") ? + function( text ) { + return text == null ? + "" : + core_trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var type, + ret = results || []; + + if ( arr != null ) { + // The window, strings (and functions) also have 'length' + // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 + type = jQuery.type( arr ); + + if ( arr.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( arr ) ) { + core_push.call( ret, arr ); + } else { + jQuery.merge( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + var len; + + if ( arr ) { + if ( core_indexOf ) { + return core_indexOf.call( arr, elem, i ); + } + + len = arr.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in arr && arr[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var l = second.length, + i = first.length, + j = 0; + + if ( typeof l === "number" ) { + for ( ; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var retVal, + ret = [], + i = 0, + length = elems.length; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, key, + ret = [], + i = 0, + length = elems.length, + // jquery objects are treated as arrays + isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; + + // Go through the array, translating each of the items to their + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( key in elems ) { + value = callback( elems[ key ], key, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + } + + // Flatten any nested arrays + return ret.concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var tmp, args, proxy; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = core_slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context, args.concat( core_slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + // Multifunctional method to get and set values of a collection + // The value/s can optionally be executed if it's a function + access: function( elems, fn, key, value, chainable, emptyGet, pass ) { + var exec, + bulk = key == null, + i = 0, + length = elems.length; + + // Sets many values + if ( key && typeof key === "object" ) { + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], 1, emptyGet, value ); + } + chainable = 1; + + // Sets one value + } else if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = pass === undefined && jQuery.isFunction( value ); + + if ( bulk ) { + // Bulk operations only iterate when executing function values + if ( exec ) { + exec = fn; + fn = function( elem, key, value ) { + return exec.call( jQuery( elem ), value ); + }; + + // Otherwise they run against the entire set + } else { + fn.call( elems, value ); + fn = null; + } + } + + if ( fn ) { + for (; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + } + + chainable = 1; + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + length ? fn( elems[0], key ) : emptyGet; + }, + + now: function() { + return ( new Date() ).getTime(); + } +}); + +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called after the browser event has already occurred. + // we once tried to use readyState "interactive" here, but it caused issues like the one + // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + setTimeout( jQuery.ready, 1 ); + + // Standards-based browsers support DOMContentLoaded + } else if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else { + // Ensure firing before onload, maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var top = false; + + try { + top = window.frameElement == null && document.documentElement; + } catch(e) {} + + if ( top && top.doScroll ) { + (function doScrollCheck() { + if ( !jQuery.isReady ) { + + try { + // Use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + top.doScroll("left"); + } catch(e) { + return setTimeout( doScrollCheck, 50 ); + } + + // and execute any waiting functions + jQuery.ready(); + } + })(); + } + } + } + return readyList.promise( obj ); +}; + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); +// String to Object options format cache +var optionsCache = {}; + +// Convert String-formatted options into Object-formatted ones and store in cache +function createOptions( options ) { + var object = optionsCache[ options ] = {}; + jQuery.each( options.split( core_rspace ), function( _, flag ) { + object[ flag ] = true; + }); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + ( optionsCache[ options ] || createOptions( options ) ) : + jQuery.extend( {}, options ); + + var // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = !options.once && [], + // Fire callbacks + fire = function( data ) { + memory = options.memory && data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { + memory = false; // To prevent further calls using add + break; + } + } + firing = false; + if ( list ) { + if ( stack ) { + if ( stack.length ) { + fire( stack.shift() ); + } + } else if ( memory ) { + list = []; + } else { + self.disable(); + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + // First, we save the current length + var start = list.length; + (function add( args ) { + jQuery.each( args, function( _, arg ) { + var type = jQuery.type( arg ); + if ( type === "function" ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && type !== "string" ) { + // Inspect recursively + add( arg ); + } + }); + })( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + jQuery.each( arguments, function( _, arg ) { + var index; + while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + // Handle firing indexes + if ( firing ) { + if ( index <= firingLength ) { + firingLength--; + } + if ( index <= firingIndex ) { + firingIndex--; + } + } + } + }); + } + return this; + }, + // Control if a given callback is in the list + has: function( fn ) { + return jQuery.inArray( fn, list ) > -1; + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( list && ( !fired || stack ) ) { + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; +jQuery.extend({ + + Deferred: function( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], + [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], + [ "notify", "progress", jQuery.Callbacks("memory") ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred(function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var action = tuple[ 0 ], + fn = fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[1] ]( jQuery.isFunction( fn ) ? + function() { + var returned = fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); + } + } : + newDefer[ action ] + ); + }); + fns = null; + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[1] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] = list.fire + deferred[ tuple[0] ] = list.fire; + deferred[ tuple[0] + "With" ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = core_slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; + if( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +}); +jQuery.support = (function() { + + var support, + all, + a, + select, + opt, + input, + fragment, + eventName, + i, + isSupported, + clickFn, + div = document.createElement("div"); + + // Setup + div.setAttribute( "className", "t" ); + div.innerHTML = "
a"; + + // Support tests won't run in some limited or non-browser environments + all = div.getElementsByTagName("*"); + a = div.getElementsByTagName("a")[ 0 ]; + if ( !all || !a || !all.length ) { + return {}; + } + + // First batch of tests + select = document.createElement("select"); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName("input")[ 0 ]; + + a.style.cssText = "top:1px;float:left;opacity:.5"; + support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: ( div.firstChild.nodeType === 3 ), + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: ( a.getAttribute("href") === "/a" ), + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.5/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: ( input.value === "on" ), + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + + // Tests for enctype support on a form (#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", + + // jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode + boxModel: ( document.compatMode === "CSS1Compat" ), + + // Will be defined later + submitBubbles: true, + changeBubbles: true, + focusinBubbles: false, + deleteExpando: true, + noCloneEvent: true, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableMarginRight: true, + boxSizingReliable: true, + pixelPosition: false + }; + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { + div.attachEvent( "onclick", clickFn = function() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + support.noCloneEvent = false; + }); + div.cloneNode( true ).fireEvent("onclick"); + div.detachEvent( "onclick", clickFn ); + } + + // Check if a radio maintains its value + // after being appended to the DOM + input = document.createElement("input"); + input.value = "t"; + input.setAttribute( "type", "radio" ); + support.radioValue = input.value === "t"; + + input.setAttribute( "checked", "checked" ); + + // #11217 - WebKit loses check when the name is after the checked attribute + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + fragment = document.createDocumentFragment(); + fragment.appendChild( div.lastChild ); + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + fragment.removeChild( input ); + fragment.appendChild( div ); + + // Technique from Juriy Zaytsev + // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ + // We only care about the case where non-standard event systems + // are used, namely in IE. Short-circuiting here helps us to + // avoid an eval call (in setAttribute) which can cause CSP + // to go haywire. See: https://developer.mozilla.org/en/Security/CSP + if ( div.attachEvent ) { + for ( i in { + submit: true, + change: true, + focusin: true + }) { + eventName = "on" + i; + isSupported = ( eventName in div ); + if ( !isSupported ) { + div.setAttribute( eventName, "return;" ); + isSupported = ( typeof div[ eventName ] === "function" ); + } + support[ i + "Bubbles" ] = isSupported; + } + } + + // Run tests that need a body at doc ready + jQuery(function() { + var container, div, tds, marginDiv, + divReset = "padding:0;margin:0;border:0;display:block;overflow:hidden;", + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + container = document.createElement("div"); + container.style.cssText = "visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px"; + body.insertBefore( container, body.firstChild ); + + // Construct the test element + div = document.createElement("div"); + container.appendChild( div ); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + div.innerHTML = "
t
"; + tds = div.getElementsByTagName("td"); + tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE <= 8 fail this test) + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Check box-sizing and margin behavior + div.innerHTML = ""; + div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; + support.boxSizing = ( div.offsetWidth === 4 ); + support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 ); + + // NOTE: To any future maintainer, we've window.getComputedStyle + // because jsdom on node.js will break without it. + if ( window.getComputedStyle ) { + support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; + support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. For more + // info see bug #3333 + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + marginDiv = document.createElement("div"); + marginDiv.style.cssText = div.style.cssText = divReset; + marginDiv.style.marginRight = marginDiv.style.width = "0"; + div.style.width = "1px"; + div.appendChild( marginDiv ); + support.reliableMarginRight = + !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); + } + + if ( typeof div.style.zoom !== "undefined" ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.innerHTML = ""; + div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = "block"; + div.style.overflow = "visible"; + div.innerHTML = "
"; + div.firstChild.style.width = "5px"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + + container.style.zoom = 1; + } + + // Null elements to avoid leaks in IE + body.removeChild( container ); + container = div = tds = marginDiv = null; + }); + + // Null elements to avoid leaks in IE + fragment.removeChild( div ); + all = a = select = opt = input = fragment = div = null; + + return support; +})(); +var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, + rmultiDash = /([A-Z])/g; + +jQuery.extend({ + cache: {}, + + deletedIds: [], + + // Remove at next major release (1.9/2.0) + uuid: 0, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ internalKey ] = id = jQuery.deletedIds.pop() || jQuery.guid++; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; + }, + + removeData: function( elem, name, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, l, + + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split(" "); + } + } + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject( cache[ id ] ) ) { + return; + } + } + + // Destroy the cache + if ( isNode ) { + jQuery.cleanData( [ elem ], true ); + + // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) + } else if ( jQuery.support.deleteExpando || cache != cache.window ) { + delete cache[ id ]; + + // When all else fails, null + } else { + cache[ id ] = null; + } + }, + + // For internal use only. + _data: function( elem, name, data ) { + return jQuery.data( elem, name, data, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; + + // nodes accept data unless otherwise specified; rejection can be conditional + return !noData || noData !== true && elem.getAttribute("classid") === noData; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var parts, part, attr, name, l, + elem = this[0], + i = 0, + data = null; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + attr = elem.attributes; + for ( l = attr.length; i < l; i++ ) { + name = attr[i].name; + + if ( !name.indexOf( "data-" ) ) { + name = jQuery.camelCase( name.substring(5) ); + + dataAttr( elem, name, data[ name ] ); + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + parts = key.split( ".", 2 ); + parts[1] = parts[1] ? "." + parts[1] : ""; + part = parts[1] + "!"; + + return jQuery.access( this, function( value ) { + + if ( value === undefined ) { + data = this.triggerHandler( "getData" + part, [ parts[0] ] ); + + // Try to fetch any internally stored data first + if ( data === undefined && elem ) { + data = jQuery.data( elem, key ); + data = dataAttr( elem, key, data ); + } + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + } + + parts[1] = value; + this.each(function() { + var self = jQuery( this ); + + self.triggerHandler( "setData" + part, parts ); + jQuery.data( this, key, value ); + self.triggerHandler( "changeData" + part, parts ); + }); + }, null, value, arguments.length > 1, null, false ); + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + var name; + for ( name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} +jQuery.extend({ + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray(data) ) { + queue = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // not intended for public consumption - generates a queueHooks object, or returns the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return jQuery._data( elem, key ) || jQuery._data( elem, key, { + empty: jQuery.Callbacks("once memory").add(function() { + jQuery.removeData( elem, type + "queue", true ); + jQuery.removeData( elem, key, true ); + }) + }); + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + // ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while( i-- ) { + tmp = jQuery._data( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +}); +var nodeHook, boolHook, fixSpecified, + rclass = /[\t\r\n]/g, + rreturn = /\r/g, + rtype = /^(?:button|input)$/i, + rfocusable = /^(?:button|input|object|select|textarea)$/i, + rclickable = /^a(?:rea|)$/i, + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classNames, i, l, elem, + setClass, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call(this, j, this.className) ); + }); + } + + if ( value && typeof value === "string" ) { + classNames = value.split( core_rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 ) { + if ( !elem.className && classNames.length === 1 ) { + elem.className = value; + + } else { + setClass = " " + elem.className + " "; + + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + if ( setClass.indexOf( " " + classNames[ c ] + " " ) < 0 ) { + setClass += classNames[ c ] + " "; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var removes, className, elem, c, cl, i, l; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call(this, j, this.className) ); + }); + } + if ( (value && typeof value === "string") || value === undefined ) { + removes = ( value || "" ).split( core_rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + if ( elem.nodeType === 1 && elem.className ) { + + className = (" " + elem.className + " ").replace( rclass, " " ); + + // loop over each item in the removal list + for ( c = 0, cl = removes.length; c < cl; c++ ) { + // Remove until there is nothing to remove, + while ( className.indexOf(" " + removes[ c ] + " ") >= 0 ) { + className = className.replace( " " + removes[ c ] + " " , " " ); + } + } + elem.className = value ? jQuery.trim( className ) : ""; + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.split( core_rspace ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space separated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // toggle whole className + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var hooks, ret, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var val, + self = jQuery(this); + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, self.val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, option, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one" || index < 0, + values = one ? null : [], + max = one ? index + 1 : options.length, + i = index < 0 ? + max : + one ? index : 0; + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // oldIE doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + // Don't return options that are disabled or in a disabled optgroup + ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && + ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var values = jQuery.makeArray( value ); + + jQuery(elem).find("option").each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + // Unused in 1.8, left in so attrFn-stabbers won't die; remove in 1.9 + attrFn: {}, + + attr: function( elem, name, value, pass ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( pass && jQuery.isFunction( jQuery.fn[ name ] ) ) { + return jQuery( elem )[ name ]( value ); + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + + } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, value + "" ); + return value; + } + + } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + + ret = elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return ret === null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var propName, attrNames, name, isBool, + i = 0; + + if ( value && elem.nodeType === 1 ) { + + attrNames = value.split( core_rspace ); + + for ( ; i < attrNames.length; i++ ) { + name = attrNames[ i ]; + + if ( name ) { + propName = jQuery.propFix[ name ] || name; + isBool = rboolean.test( name ); + + // See #9699 for explanation of this approach (setting first, then removal) + // Do not do this for boolean attributes (see #10870) + if ( !isBool ) { + jQuery.attr( elem, name, "" ); + } + elem.removeAttribute( getSetAttribute ? name : propName ); + + // Set corresponding property to false for boolean attributes + if ( isBool && propName in elem ) { + elem[ propName ] = false; + } + } + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to it's default in case type is set after value + // This is for element creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + }, + // Use the value property for back compat + // Use the nodeHook for button elements in IE6/7 (#1954) + value: { + get: function( elem, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.get( elem, name ); + } + return name in elem ? + elem.value : + null; + }, + set: function( elem, value, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.set( elem, value, name ); + } + // Does not return so that setAttribute is also used + elem.value = value; + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + var attributeNode = elem.getAttributeNode("tabindex"); + + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + } + } +}); + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + // Align boolean attributes with corresponding properties + // Fall back to attribute presence where some booleans are not supported + var attrNode, + property = jQuery.prop( elem, name ); + return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + var propName; + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + // value is true since we know at this point it's type boolean and not false + // Set boolean attributes to the same name and set the DOM property + propName = jQuery.propFix[ name ] || name; + if ( propName in elem ) { + // Only set the IDL specifically if it already exists on the element + elem[ propName ] = true; + } + + elem.setAttribute( name, name.toLowerCase() ); + } + return name; + } +}; + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + fixSpecified = { + name: true, + id: true, + coords: true + }; + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret; + ret = elem.getAttributeNode( name ); + return ret && ( fixSpecified[ name ] ? ret.value !== "" : ret.specified ) ? + ret.value : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + ret = document.createAttribute( name ); + elem.setAttributeNode( ret ); + } + return ( ret.value = value + "" ); + } + }; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }); + }); + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + if ( value === "" ) { + value = "false"; + } + nodeHook.set( elem, value, name ); + } + }; +} + + +// Some attributes require a special call on IE +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret === null ? undefined : ret; + } + }); + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Normalize to lowercase since IE uppercases css property names + return elem.style.cssText.toLowerCase() || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = value + "" ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }); +} + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }); +}); +var rformElems = /^(?:textarea|input|select)$/i, + rtypenamespace = /^([^\.]*|)(?:\.(.+)|)$/, + rhoverHack = /(?:^|\s)hover(\.\S+|)\b/, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + hoverHack = function( events ) { + return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); + }; + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + add: function( elem, types, handler, data, selector ) { + + var elemData, eventHandle, events, + t, tns, type, namespaces, handleObj, + handleObjIn, handlers, special; + + // Don't attach events to noData or text/comment nodes (allow plain objects tho) + if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + events = elemData.events; + if ( !events ) { + elemData.events = events = {}; + } + eventHandle = elemData.handle; + if ( !eventHandle ) { + elemData.handle = eventHandle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = jQuery.trim( hoverHack(types) ).split( " " ); + for ( t = 0; t < types.length; t++ ) { + + tns = rtypenamespace.exec( types[t] ) || []; + type = tns[1]; + namespaces = ( tns[2] || "" ).split( "." ).sort(); + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: tns[1], + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + handlers = events[ type ]; + if ( !handlers ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + global: {}, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var t, tns, type, origType, namespaces, origCount, + j, events, special, eventType, handleObj, + elemData = jQuery.hasData( elem ) && jQuery._data( elem ); + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = jQuery.trim( hoverHack( types || "" ) ).split(" "); + for ( t = 0; t < types.length; t++ ) { + tns = rtypenamespace.exec( types[t] ) || []; + type = origType = tns[1]; + namespaces = tns[2]; + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector? special.delegateType : special.bindType ) || type; + eventType = events[ type ] || []; + origCount = eventType.length; + namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.|)") + "(\\.|$)") : null; + + // Remove matching events + for ( j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !namespaces || namespaces.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + eventType.splice( j--, 1 ); + + if ( handleObj.selector ) { + eventType.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( eventType.length === 0 && origCount !== eventType.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + delete elemData.handle; + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery.removeData( elem, "events", true ); + } + }, + + // Events that are safe to short-circuit if no handlers are attached. + // Native DOM events should not be added, they may have inline handlers. + customEvent: { + "getData": true, + "setData": true, + "changeData": true + }, + + trigger: function( event, data, elem, onlyHandlers ) { + // Don't do events on text and comment nodes + if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { + return; + } + + // Event object or event type + var cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType, + type = event.type || event, + namespaces = []; + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "!" ) >= 0 ) { + // Exclusive events trigger only for the exact event (no namespaces) + type = type.slice(0, -1); + exclusive = true; + } + + if ( type.indexOf( "." ) >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + + if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { + // No jQuery handlers for this event type, and it can't have inline handlers + return; + } + + // Caller can pass in an Event, Object, or just an event type string + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + new jQuery.Event( type, event ) : + // Just the event type (string) + new jQuery.Event( type ); + + event.type = type; + event.isTrigger = true; + event.exclusive = exclusive; + event.namespace = namespaces.join( "." ); + event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)") : null; + ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; + + // Handle a global trigger + if ( !elem ) { + + // TODO: Stop taunting the data cache; remove global events and always attach to document + cache = jQuery.cache; + for ( i in cache ) { + if ( cache[ i ].events && cache[ i ].events[ type ] ) { + jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); + } + } + return; + } + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data != null ? jQuery.makeArray( data ) : []; + data.unshift( event ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + eventPath = [[ elem, special.bindType || type ]]; + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; + for ( old = elem; cur; cur = cur.parentNode ) { + eventPath.push([ cur, bubbleType ]); + old = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( old === (elem.ownerDocument || document) ) { + eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); + } + } + + // Fire handlers on the event path + for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { + + cur = eventPath[i][0]; + event.type = eventPath[i][1]; + + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + // Note that this is a bare JS function and not a jQuery handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + // IE<9 dies on focus/blur to hidden element (#1486) + if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + old = elem[ ontype ]; + + if ( old ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; + + if ( old ) { + elem[ ontype ] = old; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event || window.event ); + + var i, j, cur, ret, selMatch, matched, matches, handleObj, sel, related, + handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), + delegateCount = handlers.delegateCount, + args = core_slice.call( arguments ), + run_all = !event.exclusive && !event.namespace, + special = jQuery.event.special[ event.type ] || {}, + handlerQueue = []; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers that should run if there are delegated events + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && !(event.button && event.type === "click") ) { + + for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { + + // Don't process clicks (ONLY) on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.disabled !== true || event.type !== "click" ) { + selMatch = {}; + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + sel = handleObj.selector; + + if ( selMatch[ sel ] === undefined ) { + selMatch[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) >= 0 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( selMatch[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, matches: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( handlers.length > delegateCount ) { + handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); + } + + // Run delegates first; they may want to stop propagation beneath us + for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { + matched = handlerQueue[ i ]; + event.currentTarget = matched.elem; + + for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { + handleObj = matched.matches[ j ]; + + // Triggered event must either 1) be non-exclusive and have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { + + event.data = handleObj.data; + event.handleObj = handleObj; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** + props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, + originalEvent = event, + fixHook = jQuery.event.fixHooks[ event.type ] || {}, + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = jQuery.Event( originalEvent ); + + for ( i = copy.length; i; ) { + prop = copy[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Target should not be a text node (#504, Safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // For mouse/key events, metaKey==false if it's undefined (#3368, #11328; IE6/7/8) + event.metaKey = !!event.metaKey; + + return fixHook.filter? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + + focus: { + delegateType: "focusin" + }, + blur: { + delegateType: "focusout" + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( jQuery.isWindow( this ) ) { + this.onbeforeunload = eventHandle; + } + }, + + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +// Some plugins are using, but it's undocumented/deprecated and will be removed. +// The 1.7 special event interface should provide all the hooks needed now. +jQuery.event.handle = jQuery.event.dispatch; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + var name = "on" + type; + + if ( elem.detachEvent ) { + + // #8545, #7054, preventing memory leaks for custom events in IE6-8 + // detachEvent needed property on element, by name of that event, to properly expose it to GC + if ( typeof elem[ name ] === "undefined" ) { + elem[ name ] = null; + } + + elem.detachEvent( name, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // otherwise set the returnValue property of the original event to false (IE) + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj, + selector = handleObj.selector; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !jQuery._data( form, "_submit_attached" ) ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + jQuery._data( form, "_submit_attached", true ); + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + } + // Allow triggered, simulated change events (#11500) + jQuery.event.simulate( "change", this, event, true ); + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "_change_attached" ) ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + jQuery._data( elem, "_change_attached", true ); + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return !rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { // && selector != null + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + live: function( types, data, fn ) { + jQuery( this.context ).on( types, this.selector, data, fn ); + return this; + }, + die: function( types, fn ) { + jQuery( this.context ).off( types, this.selector || "**", fn ); + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.on( types, selector, data, fn ); + }, + undelegate: function( selector, types, fn ) { + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + if ( this[0] ) { + return jQuery.event.trigger( type, data, this[0], true ); + } + }, + + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, + guid = fn.guid || jQuery.guid++, + i = 0, + toggler = function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + }; + + // link all the functions, so any of them can unbind this click handler + toggler.guid = guid; + while ( i < args.length ) { + args[ i++ ].guid = guid; + } + + return this.click( toggler ); + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + if ( fn == null ) { + fn = data; + data = null; + } + + return arguments.length > 0 ? + this.on( name, null, data, fn ) : + this.trigger( name ); + }; + + if ( rkeyEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; + } + + if ( rmouseEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; + } +}); +/*! + * Sizzle CSS Selector Engine + * Copyright 2012 jQuery Foundation and other contributors + * Released under the MIT license + * http://sizzlejs.com/ + */ +(function( window, undefined ) { + +var cachedruns, + assertGetIdNotName, + Expr, + getText, + isXML, + contains, + compile, + sortOrder, + hasDuplicate, + outermostContext, + + baseHasDuplicate = true, + strundefined = "undefined", + + expando = ( "sizcache" + Math.random() ).replace( ".", "" ), + + Token = String, + document = window.document, + docElem = document.documentElement, + dirruns = 0, + done = 0, + pop = [].pop, + push = [].push, + slice = [].slice, + // Use a stripped-down indexOf if a native one is unavailable + indexOf = [].indexOf || function( elem ) { + var i = 0, + len = this.length; + for ( ; i < len; i++ ) { + if ( this[i] === elem ) { + return i; + } + } + return -1; + }, + + // Augment a function for special use by Sizzle + markFunction = function( fn, value ) { + fn[ expando ] = value == null || value; + return fn; + }, + + createCache = function() { + var cache = {}, + keys = []; + + return markFunction(function( key, value ) { + // Only keep the most recent entries + if ( keys.push( key ) > Expr.cacheLength ) { + delete cache[ keys.shift() ]; + } + + // Retrieve with (key + " ") to avoid collision with native Object.prototype properties (see Issue #157) + return (cache[ key + " " ] = value); + }, cache ); + }, + + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + + // Regex + + // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + // http://www.w3.org/TR/css3-syntax/#characters + characterEncoding = "(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+", + + // Loosely modeled on CSS identifier characters + // An unquoted value should be a CSS identifier (http://www.w3.org/TR/css3-selectors/#attribute-selectors) + // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = characterEncoding.replace( "w", "w#" ), + + // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors + operators = "([*^$|!~]?=)", + attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + + "*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", + + // Prefer arguments not in parens/brackets, + // then attribute selectors and non-pseudos (denoted by :), + // then anything else + // These preferences are here to reduce the number of selectors + // needing tokenize in the PSEUDO preFilter + pseudos = ":(" + characterEncoding + ")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:" + attributes + ")|[^:]|\\\\.)*|.*))\\)|)", + + // For matchExpr.POS and matchExpr.needsContext + pos = ":(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ), + rpseudo = new RegExp( pseudos ), + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/, + + rnot = /^:not/, + rsibling = /[\x20\t\r\n\f]*[+~]/, + rendsWithNot = /:not\($/, + + rheader = /h\d/i, + rinputs = /input|select|textarea|button/i, + + rbackslash = /\\(?!\\)/g, + + matchExpr = { + "ID": new RegExp( "^#(" + characterEncoding + ")" ), + "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), + "NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ), + "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "POS": new RegExp( pos, "i" ), + "CHILD": new RegExp( "^:(only|nth|first|last)-child(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + // For use in libraries implementing .is() + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|" + pos, "i" ) + }, + + // Support + + // Used for testing something on an element + assert = function( fn ) { + var div = document.createElement("div"); + + try { + return fn( div ); + } catch (e) { + return false; + } finally { + // release memory in IE + div = null; + } + }, + + // Check if getElementsByTagName("*") returns only elements + assertTagNameNoComments = assert(function( div ) { + div.appendChild( document.createComment("") ); + return !div.getElementsByTagName("*").length; + }), + + // Check if getAttribute returns normalized href attributes + assertHrefNotNormalized = assert(function( div ) { + div.innerHTML = ""; + return div.firstChild && typeof div.firstChild.getAttribute !== strundefined && + div.firstChild.getAttribute("href") === "#"; + }), + + // Check if attributes should be retrieved by attribute nodes + assertAttributes = assert(function( div ) { + div.innerHTML = ""; + var type = typeof div.lastChild.getAttribute("multiple"); + // IE8 returns a string for some attributes even when not present + return type !== "boolean" && type !== "string"; + }), + + // Check if getElementsByClassName can be trusted + assertUsableClassName = assert(function( div ) { + // Opera can't find a second classname (in 9.6) + div.innerHTML = ""; + if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) { + return false; + } + + // Safari 3.2 caches class attributes and doesn't catch changes + div.lastChild.className = "e"; + return div.getElementsByClassName("e").length === 2; + }), + + // Check if getElementById returns elements by name + // Check if getElementsByName privileges form controls or returns elements by ID + assertUsableName = assert(function( div ) { + // Inject content + div.id = expando + 0; + div.innerHTML = "
"; + docElem.insertBefore( div, docElem.firstChild ); + + // Test + var pass = document.getElementsByName && + // buggy browsers will return fewer than the correct 2 + document.getElementsByName( expando ).length === 2 + + // buggy browsers will return more than the correct 0 + document.getElementsByName( expando + 0 ).length; + assertGetIdNotName = !document.getElementById( expando ); + + // Cleanup + docElem.removeChild( div ); + + return pass; + }); + +// If slice is not available, provide a backup +try { + slice.call( docElem.childNodes, 0 )[0].nodeType; +} catch ( e ) { + slice = function( i ) { + var elem, + results = []; + for ( ; (elem = this[i]); i++ ) { + results.push( elem ); + } + return results; + }; +} + +function Sizzle( selector, context, results, seed ) { + results = results || []; + context = context || document; + var match, elem, xml, m, + nodeType = context.nodeType; + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + if ( nodeType !== 1 && nodeType !== 9 ) { + return []; + } + + xml = isXML( context ); + + if ( !xml && !seed ) { + if ( (match = rquickExpr.exec( selector )) ) { + // Speed-up: Sizzle("#ID") + if ( (m = match[1]) ) { + if ( nodeType === 9 ) { + elem = context.getElementById( m ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE, Opera, and Webkit return items + // by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + } else { + // Context is not a document + if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && + contains( context, elem ) && elem.id === m ) { + results.push( elem ); + return results; + } + } + + // Speed-up: Sizzle("TAG") + } else if ( match[2] ) { + push.apply( results, slice.call(context.getElementsByTagName( selector ), 0) ); + return results; + + // Speed-up: Sizzle(".CLASS") + } else if ( (m = match[3]) && assertUsableClassName && context.getElementsByClassName ) { + push.apply( results, slice.call(context.getElementsByClassName( m ), 0) ); + return results; + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed, xml ); +} + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + return Sizzle( expr, null, null, [ elem ] ).length > 0; +}; + +// Returns a function to use in pseudos for input types +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +// Returns a function to use in pseudos for buttons +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +// Returns a function to use in pseudos for positionals +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( nodeType ) { + if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (see #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + } else { + + // If no nodeType, this is expected to be an array + for ( ; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } + return ret; +}; + +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +// Element contains another +contains = Sizzle.contains = docElem.contains ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && adown.contains && adown.contains(bup) ); + } : + docElem.compareDocumentPosition ? + function( a, b ) { + return b && !!( a.compareDocumentPosition( b ) & 16 ); + } : + function( a, b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + return false; + }; + +Sizzle.attr = function( elem, name ) { + var val, + xml = isXML( elem ); + + if ( !xml ) { + name = name.toLowerCase(); + } + if ( (val = Expr.attrHandle[ name ]) ) { + return val( elem ); + } + if ( xml || assertAttributes ) { + return elem.getAttribute( name ); + } + val = elem.getAttributeNode( name ); + return val ? + typeof elem[ name ] === "boolean" ? + elem[ name ] ? name : null : + val.specified ? val.value : null : + null; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + // IE6/7 return a modified href + attrHandle: assertHrefNotNormalized ? + {} : + { + "href": function( elem ) { + return elem.getAttribute( "href", 2 ); + }, + "type": function( elem ) { + return elem.getAttribute("type"); + } + }, + + find: { + "ID": assertGetIdNotName ? + function( id, context, xml ) { + if ( typeof context.getElementById !== strundefined && !xml ) { + var m = context.getElementById( id ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + } : + function( id, context, xml ) { + if ( typeof context.getElementById !== strundefined && !xml ) { + var m = context.getElementById( id ); + + return m ? + m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ? + [m] : + undefined : + []; + } + }, + + "TAG": assertTagNameNoComments ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== strundefined ) { + return context.getElementsByTagName( tag ); + } + } : + function( tag, context ) { + var results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + var elem, + tmp = [], + i = 0; + + for ( ; (elem = results[i]); i++ ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }, + + "NAME": assertUsableName && function( tag, context ) { + if ( typeof context.getElementsByName !== strundefined ) { + return context.getElementsByName( name ); + } + }, + + "CLASS": assertUsableClassName && function( className, context, xml ) { + if ( typeof context.getElementsByClassName !== strundefined && !xml ) { + return context.getElementsByClassName( className ); + } + } + }, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( rbackslash, "" ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[4] || match[5] || "" ).replace( rbackslash, "" ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 3 xn-component of xn+y argument ([+-]?\d*n|) + 4 sign of xn-component + 5 x of xn-component + 6 sign of y-component + 7 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1] === "nth" ) { + // nth-child requires argument + if ( !match[2] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[3] = +( match[3] ? match[4] + (match[5] || 1) : 2 * ( match[2] === "even" || match[2] === "odd" ) ); + match[4] = +( ( match[6] + match[7] ) || match[2] === "odd" ); + + // other types prohibit arguments + } else if ( match[2] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var unquoted, excess; + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + if ( match[3] ) { + match[2] = match[3]; + } else if ( (unquoted = match[4]) ) { + // Only check arguments that contain a pseudo + if ( rpseudo.test(unquoted) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + unquoted = unquoted.slice( 0, excess ); + match[0] = match[0].slice( 0, excess ); + } + match[2] = unquoted; + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + "ID": assertGetIdNotName ? + function( id ) { + id = id.replace( rbackslash, "" ); + return function( elem ) { + return elem.getAttribute("id") === id; + }; + } : + function( id ) { + id = id.replace( rbackslash, "" ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); + return node && node.value === id; + }; + }, + + "TAG": function( nodeName ) { + if ( nodeName === "*" ) { + return function() { return true; }; + } + nodeName = nodeName.replace( rbackslash, "" ).toLowerCase(); + + return function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ expando ][ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem, context ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.substr( result.length - check.length ) === check : + operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.substr( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, argument, first, last ) { + + if ( type === "nth" ) { + return function( elem ) { + var node, diff, + parent = elem.parentNode; + + if ( first === 1 && last === 0 ) { + return true; + } + + if ( parent ) { + diff = 0; + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + diff++; + if ( elem === node ) { + break; + } + } + } + } + + // Incorporate the offset (or cast to NaN), then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + }; + } + + return function( elem ) { + var node = elem; + + switch ( type ) { + case "only": + case "first": + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + if ( type === "first" ) { + return true; + } + + node = elem; + + /* falls through */ + case "last": + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + return true; + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf.call( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + "enabled": function( elem ) { + return elem.disabled === false; + }, + + "disabled": function( elem ) { + return elem.disabled === true; + }, + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), + // not comment, processing instructions, or others + // Thanks to Diego Perini for the nodeName shortcut + // Greater than "@" means alpha characters (specifically not starting with "#" or "?") + var nodeType; + elem = elem.firstChild; + while ( elem ) { + if ( elem.nodeName > "@" || (nodeType = elem.nodeType) === 3 || nodeType === 4 ) { + return false; + } + elem = elem.nextSibling; + } + return true; + }, + + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "text": function( elem ) { + var type, attr; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && + (type = elem.type) === "text" && + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === type ); + }, + + // Input types + "radio": createInputPseudo("radio"), + "checkbox": createInputPseudo("checkbox"), + "file": createInputPseudo("file"), + "password": createInputPseudo("password"), + "image": createInputPseudo("image"), + + "submit": createButtonPseudo("submit"), + "reset": createButtonPseudo("reset"), + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "focus": function( elem ) { + var doc = elem.ownerDocument; + return elem === doc.activeElement && (!doc.hasFocus || doc.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + "active": function( elem ) { + return elem === elem.ownerDocument.activeElement; + }, + + // Positional types + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + for ( var i = 0; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + for ( var i = 1; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + for ( var i = argument < 0 ? argument + length : argument; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + for ( var i = argument < 0 ? argument + length : argument; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +function siblingCheck( a, b, ret ) { + if ( a === b ) { + return ret; + } + + var cur = a.nextSibling; + + while ( cur ) { + if ( cur === b ) { + return -1; + } + + cur = cur.nextSibling; + } + + return 1; +} + +sortOrder = docElem.compareDocumentPosition ? + function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + return ( !a.compareDocumentPosition || !b.compareDocumentPosition ? + a.compareDocumentPosition : + a.compareDocumentPosition(b) & 4 + ) ? -1 : 1; + } : + function( a, b ) { + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Fallback to using sourceIndex (in IE) if it's available on both nodes + } else if ( a.sourceIndex && b.sourceIndex ) { + return a.sourceIndex - b.sourceIndex; + } + + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; + + // If the nodes are siblings (or identical) we can do a quick check + if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; + } + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; + } + + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; + } + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); + }; + +// Always assume the presence of duplicates if sort doesn't +// pass them to our comparison function (as in Google Chrome). +[0, 0].sort( sortOrder ); +baseHasDuplicate = !hasDuplicate; + +// Document sorting and removing duplicates +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + i = 1, + j = 0; + + hasDuplicate = baseHasDuplicate; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( ; (elem = results[i]); i++ ) { + if ( elem === results[ i - 1 ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + return results; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +function tokenize( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ expando ][ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( tokens = [] ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + tokens.push( matched = new Token( match.shift() ) ); + soFar = soFar.slice( matched.length ); + + // Cast descendant combinators to space + matched.type = match[0].replace( rtrim, " " ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + + tokens.push( matched = new Token( match.shift() ) ); + soFar = soFar.slice( matched.length ); + matched.type = type; + matched.matches = match; + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && combinator.dir === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( checkNonElements || elem.nodeType === 1 ) { + return matcher( elem, context, xml ); + } + } + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching + if ( !xml ) { + var cache, + dirkey = dirruns + " " + doneName + " ", + cachedkey = dirkey + cachedruns; + while ( (elem = elem[ dir ]) ) { + if ( checkNonElements || elem.nodeType === 1 ) { + if ( (cache = elem[ expando ]) === cachedkey ) { + return elem.sizset; + } else if ( typeof cache === "string" && cache.indexOf(dirkey) === 0 ) { + if ( elem.sizset ) { + return elem; + } + } else { + elem[ expando ] = cachedkey; + if ( matcher( elem, context, xml ) ) { + elem.sizset = true; + return elem; + } + elem.sizset = false; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( checkNonElements || elem.nodeType === 1 ) { + if ( matcher( elem, context, xml ) ) { + return elem; + } + } + } + } + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf.call( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && tokens.slice( 0, i - 1 ).join("").replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && tokens.join("") + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, expandContext ) { + var elem, j, matcher, + setMatched = [], + matchedCount = 0, + i = "0", + unmatched = seed && [], + outermost = expandContext != null, + contextBackup = outermostContext, + // We must always have either seed elements or context + elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), + // Nested matchers should use non-integer dirruns + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.E); + + if ( outermost ) { + outermostContext = context !== document && context; + cachedruns = superMatcher.el; + } + + // Add elements passing elementMatchers directly to results + for ( ; (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + for ( j = 0; (matcher = elementMatchers[j]); j++ ) { + if ( matcher( elem, context, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + cachedruns = ++superMatcher.el; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // Apply set filters to unmatched elements + matchedCount += i; + if ( bySet && i !== matchedCount ) { + for ( j = 0; (matcher = setMatchers[j]); j++ ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + superMatcher.el = 0; + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ expando ][ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !group ) { + group = tokenize( selector ); + } + i = group.length; + while ( i-- ) { + cached = matcherFromTokens( group[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + } + return cached; +}; + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function select( selector, context, results, seed, xml ) { + var i, tokens, token, type, find, + match = tokenize( selector ), + j = match.length; + + if ( !seed ) { + // Try to minimize operations if there is only one group + if ( match.length === 1 ) { + + // Take a shortcut and set the context if the root selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + context.nodeType === 9 && !xml && + Expr.relative[ tokens[1].type ] ) { + + context = Expr.find["ID"]( token.matches[0].replace( rbackslash, "" ), context, xml )[0]; + if ( !context ) { + return results; + } + + selector = selector.slice( tokens.shift().length ); + } + + // Fetch a seed set for right-to-left matching + for ( i = matchExpr["POS"].test( selector ) ? -1 : tokens.length - 1; i >= 0; i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( rbackslash, "" ), + rsibling.test( tokens[0].type ) && context.parentNode || context, + xml + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && tokens.join(""); + if ( !selector ) { + push.apply( results, slice.call( seed, 0 ) ); + return results; + } + + break; + } + } + } + } + } + + // Compile and execute a filtering function + // Provide `match` to avoid retokenization if we modified the selector above + compile( selector, match )( + seed, + context, + xml, + results, + rsibling.test( selector ) + ); + return results; +} + +if ( document.querySelectorAll ) { + (function() { + var disconnectedMatch, + oldSelect = select, + rescape = /'|\\/g, + rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g, + + // qSa(:focus) reports false when true (Chrome 21), no need to also add to buggyMatches since matches checks buggyQSA + // A support test would require too much code (would include document ready) + rbuggyQSA = [ ":focus" ], + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + // A support test would require too much code (would include document ready) + // just skip matchesSelector for :active + rbuggyMatches = [ ":active" ], + matches = docElem.matchesSelector || + docElem.mozMatchesSelector || + docElem.webkitMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector; + + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explictly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + div.innerHTML = ""; + + // IE8 - Some boolean attributes are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here (do not put tests after this one) + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + }); + + assert(function( div ) { + + // Opera 10-12/IE9 - ^= $= *= and empty values + // Should not select anything + div.innerHTML = "

"; + if ( div.querySelectorAll("[test^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here (do not put tests after this one) + div.innerHTML = ""; + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push(":enabled", ":disabled"); + } + }); + + // rbuggyQSA always contains :focus, so no need for a length check + rbuggyQSA = /* rbuggyQSA.length && */ new RegExp( rbuggyQSA.join("|") ); + + select = function( selector, context, results, seed, xml ) { + // Only use querySelectorAll when not filtering, + // when this is not xml, + // and when no QSA bugs apply + if ( !seed && !xml && !rbuggyQSA.test( selector ) ) { + var groups, i, + old = true, + nid = expando, + newContext = context, + newSelector = context.nodeType === 9 && selector; + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + groups = tokenize( selector ); + + if ( (old = context.getAttribute("id")) ) { + nid = old.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", nid ); + } + nid = "[id='" + nid + "'] "; + + i = groups.length; + while ( i-- ) { + groups[i] = nid + groups[i].join(""); + } + newContext = rsibling.test( selector ) && context.parentNode || context; + newSelector = groups.join(","); + } + + if ( newSelector ) { + try { + push.apply( results, slice.call( newContext.querySelectorAll( + newSelector + ), 0 ) ); + return results; + } catch(qsaError) { + } finally { + if ( !old ) { + context.removeAttribute("id"); + } + } + } + } + + return oldSelect( selector, context, results, seed, xml ); + }; + + if ( matches ) { + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + disconnectedMatch = matches.call( div, "div" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + try { + matches.call( div, "[test!='']:sizzle" ); + rbuggyMatches.push( "!=", pseudos ); + } catch ( e ) {} + }); + + // rbuggyMatches always contains :active and :focus, so no need for a length check + rbuggyMatches = /* rbuggyMatches.length && */ new RegExp( rbuggyMatches.join("|") ); + + Sizzle.matchesSelector = function( elem, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + // rbuggyMatches always contains :active, so no need for an existence check + if ( !isXML( elem ) && !rbuggyMatches.test( expr ) && !rbuggyQSA.test( expr ) ) { + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch(e) {} + } + + return Sizzle( expr, null, null, [ elem ] ).length > 0; + }; + } + })(); +} + +// Deprecated +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Back-compat +function setFilters() {} +Expr.filters = setFilters.prototype = Expr.pseudos; +Expr.setFilters = new setFilters(); + +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.pseudos; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})( window ); +var runtil = /Until$/, + rparentsprev = /^(?:parents|prev(?:Until|All))/, + isSimple = /^.[^:#\[\.,]*$/, + rneedsContext = jQuery.expr.match.needsContext, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var i, l, length, n, r, ret, + self = this; + + if ( typeof selector !== "string" ) { + return jQuery( selector ).filter(function() { + for ( i = 0, l = self.length; i < l; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }); + } + + ret = this.pushStack( "", "find", selector ); + + for ( i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( n = length; n < ret.length; n++ ) { + for ( r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return ret; + }, + + has: function( target ) { + var i, + targets = jQuery( target, this ), + len = targets.length; + + return this.filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && ( + typeof selector === "string" ? + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + rneedsContext.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + ret = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + cur = this[i]; + + while ( cur && cur.ownerDocument && cur !== context && cur.nodeType !== 11 ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + } + cur = cur.parentNode; + } + } + + ret = ret.length > 1 ? jQuery.unique( ret ) : ret; + + return this.pushStack( ret, "closest", selectors ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + } +}); + +jQuery.fn.andSelf = jQuery.fn.addBack; + +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} + +function sibling( cur, dir ) { + do { + cur = cur[ dir ]; + } while ( cur && cur.nodeType !== 1 ); + + return cur; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + + if ( this.length > 1 && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret, name, core_slice.call( arguments ).join(",") ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return ( elem === qualifier ) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem, i ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + }); +} +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + rtagName = /<([\w:]+)/, + rtbody = /]", "i"), + rcheckableType = /^(?:checkbox|radio)$/, + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /\/(java|ecma)script/i, + rcleanScript = /^\s*\s*$/g, + wrapMap = { + option: [ 1, "" ], + legend: [ 1, "
", "
" ], + thead: [ 1, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + col: [ 2, "", "
" ], + area: [ 1, "", "" ], + _default: [ 0, "", "" ] + }, + safeFragment = createSafeFragment( document ), + fragmentDiv = safeFragment.appendChild( document.createElement("div") ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, +// unless wrapped in a div with non-breaking characters in front of it. +if ( !jQuery.support.htmlSerialize ) { + wrapMap._default = [ 1, "X
", "
" ]; +} + +jQuery.fn.extend({ + text: function( value ) { + return jQuery.access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); + }, null, value, arguments.length ); + }, + + wrapAll: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapAll( html.call(this, i) ); + }); + } + + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); + + if ( this[0].parentNode ) { + wrap.insertBefore( this[0] ); + } + + wrap.map(function() { + var elem = this; + + while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { + elem = elem.firstChild; + } + + return elem; + }).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapInner( html.call(this, i) ); + }); + } + + return this.each(function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + }); + }, + + wrap: function( html ) { + var isFunction = jQuery.isFunction( html ); + + return this.each(function(i) { + jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); + }); + }, + + unwrap: function() { + return this.parent().each(function() { + if ( !jQuery.nodeName( this, "body" ) ) { + jQuery( this ).replaceWith( this.childNodes ); + } + }).end(); + }, + + append: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 ) { + this.appendChild( elem ); + } + }); + }, + + prepend: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 ) { + this.insertBefore( elem, this.firstChild ); + } + }); + }, + + before: function() { + if ( !isDisconnected( this[0] ) ) { + return this.domManip(arguments, false, function( elem ) { + this.parentNode.insertBefore( elem, this ); + }); + } + + if ( arguments.length ) { + var set = jQuery.clean( arguments ); + return this.pushStack( jQuery.merge( set, this ), "before", this.selector ); + } + }, + + after: function() { + if ( !isDisconnected( this[0] ) ) { + return this.domManip(arguments, false, function( elem ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + }); + } + + if ( arguments.length ) { + var set = jQuery.clean( arguments ); + return this.pushStack( jQuery.merge( this, set ), "after", this.selector ); + } + }, + + // keepData is for internal use only--do not document + remove: function( selector, keepData ) { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + if ( !selector || jQuery.filter( selector, [ elem ] ).length ) { + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( elem.getElementsByTagName("*") ); + jQuery.cleanData( [ elem ] ); + } + + if ( elem.parentNode ) { + elem.parentNode.removeChild( elem ); + } + } + } + + return this; + }, + + empty: function() { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( elem.getElementsByTagName("*") ); + } + + // Remove any remaining nodes + while ( elem.firstChild ) { + elem.removeChild( elem.firstChild ); + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function () { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + }); + }, + + html: function( value ) { + return jQuery.access( this, function( value ) { + var elem = this[0] || {}, + i = 0, + l = this.length; + + if ( value === undefined ) { + return elem.nodeType === 1 ? + elem.innerHTML.replace( rinlinejQuery, "" ) : + undefined; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) && + ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && + !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) { + + value = value.replace( rxhtmlTag, "<$1>" ); + + try { + for (; i < l; i++ ) { + // Remove element nodes and prevent memory leaks + elem = this[i] || {}; + if ( elem.nodeType === 1 ) { + jQuery.cleanData( elem.getElementsByTagName( "*" ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch(e) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function( value ) { + if ( !isDisconnected( this[0] ) ) { + // Make sure that the elements are removed from the DOM before they are inserted + // this can help fix replacing a parent with child elements + if ( jQuery.isFunction( value ) ) { + return this.each(function(i) { + var self = jQuery(this), old = self.html(); + self.replaceWith( value.call( this, i, old ) ); + }); + } + + if ( typeof value !== "string" ) { + value = jQuery( value ).detach(); + } + + return this.each(function() { + var next = this.nextSibling, + parent = this.parentNode; + + jQuery( this ).remove(); + + if ( next ) { + jQuery(next).before( value ); + } else { + jQuery(parent).append( value ); + } + }); + } + + return this.length ? + this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) : + this; + }, + + detach: function( selector ) { + return this.remove( selector, true ); + }, + + domManip: function( args, table, callback ) { + + // Flatten any nested arrays + args = [].concat.apply( [], args ); + + var results, first, fragment, iNoClone, + i = 0, + value = args[0], + scripts = [], + l = this.length; + + // We can't cloneNode fragments that contain checked, in WebKit + if ( !jQuery.support.checkClone && l > 1 && typeof value === "string" && rchecked.test( value ) ) { + return this.each(function() { + jQuery(this).domManip( args, table, callback ); + }); + } + + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + args[0] = value.call( this, i, table ? self.html() : undefined ); + self.domManip( args, table, callback ); + }); + } + + if ( this[0] ) { + results = jQuery.buildFragment( args, this, scripts ); + fragment = results.fragment; + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + if ( first ) { + table = table && jQuery.nodeName( first, "tr" ); + + // Use the original fragment for the last item instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + // Fragments from the fragment cache must always be cloned and never used in place. + for ( iNoClone = results.cacheable || l - 1; i < l; i++ ) { + callback.call( + table && jQuery.nodeName( this[i], "table" ) ? + findOrAppend( this[i], "tbody" ) : + this[i], + i === iNoClone ? + fragment : + jQuery.clone( fragment, true, true ) + ); + } + } + + // Fix #11809: Avoid leaking memory + fragment = first = null; + + if ( scripts.length ) { + jQuery.each( scripts, function( i, elem ) { + if ( elem.src ) { + if ( jQuery.ajax ) { + jQuery.ajax({ + url: elem.src, + type: "GET", + dataType: "script", + async: false, + global: false, + "throws": true + }); + } else { + jQuery.error("no ajax"); + } + } else { + jQuery.globalEval( ( elem.text || elem.textContent || elem.innerHTML || "" ).replace( rcleanScript, "" ) ); + } + + if ( elem.parentNode ) { + elem.parentNode.removeChild( elem ); + } + }); + } + } + + return this; + } +}); + +function findOrAppend( elem, tag ) { + return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) ); +} + +function cloneCopyEvent( src, dest ) { + + if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { + return; + } + + var type, i, l, + oldData = jQuery._data( src ), + curData = jQuery._data( dest, oldData ), + events = oldData.events; + + if ( events ) { + delete curData.handle; + curData.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + + // make the cloned public data object a copy from the original + if ( curData.data ) { + curData.data = jQuery.extend( {}, curData.data ); + } +} + +function cloneFixAttributes( src, dest ) { + var nodeName; + + // We do not need to do anything for non-Elements + if ( dest.nodeType !== 1 ) { + return; + } + + // clearAttributes removes the attributes, which we don't want, + // but also removes the attachEvent events, which we *do* want + if ( dest.clearAttributes ) { + dest.clearAttributes(); + } + + // mergeAttributes, in contrast, only merges back on the + // original attributes, not the events + if ( dest.mergeAttributes ) { + dest.mergeAttributes( src ); + } + + nodeName = dest.nodeName.toLowerCase(); + + if ( nodeName === "object" ) { + // IE6-10 improperly clones children of object elements using classid. + // IE10 throws NoModificationAllowedError if parent is null, #12132. + if ( dest.parentNode ) { + dest.outerHTML = src.outerHTML; + } + + // This path appears unavoidable for IE9. When cloning an object + // element in IE9, the outerHTML strategy above is not sufficient. + // If the src has innerHTML and the destination does not, + // copy the src.innerHTML into the dest.innerHTML. #10324 + if ( jQuery.support.html5Clone && (src.innerHTML && !jQuery.trim(dest.innerHTML)) ) { + dest.innerHTML = src.innerHTML; + } + + } else if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + // IE6-8 fails to persist the checked state of a cloned checkbox + // or radio button. Worse, IE6-7 fail to give the cloned element + // a checked appearance if the defaultChecked value isn't also set + + dest.defaultChecked = dest.checked = src.checked; + + // IE6-7 get confused and end up setting the value of a cloned + // checkbox/radio button to an empty string instead of "on" + if ( dest.value !== src.value ) { + dest.value = src.value; + } + + // IE6-8 fails to return the selected option to the default selected + // state when cloning options + } else if ( nodeName === "option" ) { + dest.selected = src.defaultSelected; + + // IE6-8 fails to set the defaultValue to the correct value when + // cloning other types of input fields + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + + // IE blanks contents when cloning scripts + } else if ( nodeName === "script" && dest.text !== src.text ) { + dest.text = src.text; + } + + // Event data gets referenced instead of copied if the expando + // gets copied too + dest.removeAttribute( jQuery.expando ); +} + +jQuery.buildFragment = function( args, context, scripts ) { + var fragment, cacheable, cachehit, + first = args[ 0 ]; + + // Set context from what may come in as undefined or a jQuery collection or a node + // Updated to fix #12266 where accessing context[0] could throw an exception in IE9/10 & + // also doubles as fix for #8950 where plain objects caused createDocumentFragment exception + context = context || document; + context = !context.nodeType && context[0] || context; + context = context.ownerDocument || context; + + // Only cache "small" (1/2 KB) HTML strings that are associated with the main document + // Cloning options loses the selected state, so don't cache them + // IE 6 doesn't like it when you put or elements in a fragment + // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache + // Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501 + if ( args.length === 1 && typeof first === "string" && first.length < 512 && context === document && + first.charAt(0) === "<" && !rnocache.test( first ) && + (jQuery.support.checkClone || !rchecked.test( first )) && + (jQuery.support.html5Clone || !rnoshimcache.test( first )) ) { + + // Mark cacheable and look for a hit + cacheable = true; + fragment = jQuery.fragments[ first ]; + cachehit = fragment !== undefined; + } + + if ( !fragment ) { + fragment = context.createDocumentFragment(); + jQuery.clean( args, context, fragment, scripts ); + + // Update the cache, but only store false + // unless this is a second parsing of the same content + if ( cacheable ) { + jQuery.fragments[ first ] = cachehit && fragment; + } + } + + return { fragment: fragment, cacheable: cacheable }; +}; + +jQuery.fragments = {}; + +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + i = 0, + ret = [], + insert = jQuery( selector ), + l = insert.length, + parent = this.length === 1 && this[0].parentNode; + + if ( (parent == null || parent && parent.nodeType === 11 && parent.childNodes.length === 1) && l === 1 ) { + insert[ original ]( this[0] ); + return this; + } else { + for ( ; i < l; i++ ) { + elems = ( i > 0 ? this.clone(true) : this ).get(); + jQuery( insert[i] )[ original ]( elems ); + ret = ret.concat( elems ); + } + + return this.pushStack( ret, name, insert.selector ); + } + }; +}); + +function getAll( elem ) { + if ( typeof elem.getElementsByTagName !== "undefined" ) { + return elem.getElementsByTagName( "*" ); + + } else if ( typeof elem.querySelectorAll !== "undefined" ) { + return elem.querySelectorAll( "*" ); + + } else { + return []; + } +} + +// Used in clean, fixes the defaultChecked property +function fixDefaultChecked( elem ) { + if ( rcheckableType.test( elem.type ) ) { + elem.defaultChecked = elem.checked; + } +} + +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var srcElements, + destElements, + i, + clone; + + if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { + clone = elem.cloneNode( true ); + + // IE<=8 does not properly clone detached, unknown element nodes + } else { + fragmentDiv.innerHTML = elem.outerHTML; + fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); + } + + if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && + (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { + // IE copies events bound via attachEvent when using cloneNode. + // Calling detachEvent on the clone will also remove the events + // from the original. In order to get around this, we use some + // proprietary methods to clear the events. Thanks to MooTools + // guys for this hotness. + + cloneFixAttributes( elem, clone ); + + // Using Sizzle here is crazy slow, so we use getElementsByTagName instead + srcElements = getAll( elem ); + destElements = getAll( clone ); + + // Weird iteration because IE will replace the length property + // with an element if you are cloning the body and one of the + // elements on the page has a name or id of "length" + for ( i = 0; srcElements[i]; ++i ) { + // Ensure that the destination node is not null; Fixes #9587 + if ( destElements[i] ) { + cloneFixAttributes( srcElements[i], destElements[i] ); + } + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + cloneCopyEvent( elem, clone ); + + if ( deepDataAndEvents ) { + srcElements = getAll( elem ); + destElements = getAll( clone ); + + for ( i = 0; srcElements[i]; ++i ) { + cloneCopyEvent( srcElements[i], destElements[i] ); + } + } + } + + srcElements = destElements = null; + + // Return the cloned set + return clone; + }, + + clean: function( elems, context, fragment, scripts ) { + var i, j, elem, tag, wrap, depth, div, hasBody, tbody, len, handleScript, jsTags, + safe = context === document && safeFragment, + ret = []; + + // Ensure that context is a document + if ( !context || typeof context.createDocumentFragment === "undefined" ) { + context = document; + } + + // Use the already-created safe fragment if context permits + for ( i = 0; (elem = elems[i]) != null; i++ ) { + if ( typeof elem === "number" ) { + elem += ""; + } + + if ( !elem ) { + continue; + } + + // Convert html string into DOM nodes + if ( typeof elem === "string" ) { + if ( !rhtml.test( elem ) ) { + elem = context.createTextNode( elem ); + } else { + // Ensure a safe container in which to render the html + safe = safe || createSafeFragment( context ); + div = context.createElement("div"); + safe.appendChild( div ); + + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(rxhtmlTag, "<$1>"); + + // Go to html and back, then peel off extra wrappers + tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + depth = wrap[0]; + div.innerHTML = wrap[1] + elem + wrap[2]; + + // Move to the right depth + while ( depth-- ) { + div = div.lastChild; + } + + // Remove IE's autoinserted from table fragments + if ( !jQuery.support.tbody ) { + + // String was a , *may* have spurious + hasBody = rtbody.test(elem); + tbody = tag === "table" && !hasBody ? + div.firstChild && div.firstChild.childNodes : + + // String was a bare or + wrap[1] === "
" && !hasBody ? + div.childNodes : + []; + + for ( j = tbody.length - 1; j >= 0 ; --j ) { + if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) { + tbody[ j ].parentNode.removeChild( tbody[ j ] ); + } + } + } + + // IE completely kills leading whitespace when innerHTML is used + if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild ); + } + + elem = div.childNodes; + + // Take out of fragment container (we need a fresh div each time) + div.parentNode.removeChild( div ); + } + } + + if ( elem.nodeType ) { + ret.push( elem ); + } else { + jQuery.merge( ret, elem ); + } + } + + // Fix #11356: Clear elements from safeFragment + if ( div ) { + elem = div = safe = null; + } + + // Reset defaultChecked for any radios and checkboxes + // about to be appended to the DOM in IE 6/7 (#8060) + if ( !jQuery.support.appendChecked ) { + for ( i = 0; (elem = ret[i]) != null; i++ ) { + if ( jQuery.nodeName( elem, "input" ) ) { + fixDefaultChecked( elem ); + } else if ( typeof elem.getElementsByTagName !== "undefined" ) { + jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked ); + } + } + } + + // Append elements to a provided document fragment + if ( fragment ) { + // Special handling of each script element + handleScript = function( elem ) { + // Check if we consider it executable + if ( !elem.type || rscriptType.test( elem.type ) ) { + // Detach the script and store it in the scripts array (if provided) or the fragment + // Return truthy to indicate that it has been handled + return scripts ? + scripts.push( elem.parentNode ? elem.parentNode.removeChild( elem ) : elem ) : + fragment.appendChild( elem ); + } + }; + + for ( i = 0; (elem = ret[i]) != null; i++ ) { + // Check if we're done after handling an executable script + if ( !( jQuery.nodeName( elem, "script" ) && handleScript( elem ) ) ) { + // Append to fragment and handle embedded scripts + fragment.appendChild( elem ); + if ( typeof elem.getElementsByTagName !== "undefined" ) { + // handleScript alters the DOM, so use jQuery.merge to ensure snapshot iteration + jsTags = jQuery.grep( jQuery.merge( [], elem.getElementsByTagName("script") ), handleScript ); + + // Splice the scripts into ret after their former ancestor and advance our index beyond them + ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) ); + i += jsTags.length; + } + } + } + } + + return ret; + }, + + cleanData: function( elems, /* internal */ acceptData ) { + var data, id, elem, type, + i = 0, + internalKey = jQuery.expando, + cache = jQuery.cache, + deleteExpando = jQuery.support.deleteExpando, + special = jQuery.event.special; + + for ( ; (elem = elems[i]) != null; i++ ) { + + if ( acceptData || jQuery.acceptData( elem ) ) { + + id = elem[ internalKey ]; + data = id && cache[ id ]; + + if ( data ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Remove cache only if it was not already removed by jQuery.event.remove + if ( cache[ id ] ) { + + delete cache[ id ]; + + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( deleteExpando ) { + delete elem[ internalKey ]; + + } else if ( elem.removeAttribute ) { + elem.removeAttribute( internalKey ); + + } else { + elem[ internalKey ] = null; + } + + jQuery.deletedIds.push( id ); + } + } + } + } + } +}); +// Limit scope pollution from any deprecated API +(function() { + +var matched, browser; + +// Use of jQuery.browser is frowned upon. +// More details: http://api.jquery.com/jQuery.browser +// jQuery.uaMatch maintained for back-compat +jQuery.uaMatch = function( ua ) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) || + /(webkit)[ \/]([\w.]+)/.exec( ua ) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) || + /(msie) ([\w.]+)/.exec( ua ) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; +}; + +matched = jQuery.uaMatch( navigator.userAgent ); +browser = {}; + +if ( matched.browser ) { + browser[ matched.browser ] = true; + browser.version = matched.version; +} + +// Chrome is Webkit, but Webkit is also Safari. +if ( browser.chrome ) { + browser.webkit = true; +} else if ( browser.webkit ) { + browser.safari = true; +} + +jQuery.browser = browser; + +jQuery.sub = function() { + function jQuerySub( selector, context ) { + return new jQuerySub.fn.init( selector, context ); + } + jQuery.extend( true, jQuerySub, this ); + jQuerySub.superclass = this; + jQuerySub.fn = jQuerySub.prototype = this(); + jQuerySub.fn.constructor = jQuerySub; + jQuerySub.sub = this.sub; + jQuerySub.fn.init = function init( selector, context ) { + if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { + context = jQuerySub( context ); + } + + return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); + }; + jQuerySub.fn.init.prototype = jQuerySub.fn; + var rootjQuerySub = jQuerySub(document); + return jQuerySub; +}; + +})(); +var curCSS, iframe, iframeDoc, + ralpha = /alpha\([^)]*\)/i, + ropacity = /opacity=([^)]*)/, + rposition = /^(top|right|bottom|left)$/, + // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" + // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rmargin = /^margin/, + rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ), + rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ), + rrelNum = new RegExp( "^([-+])=(" + core_pnum + ")", "i" ), + elemdisplay = { BODY: "block" }, + + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: 0, + fontWeight: 400 + }, + + cssExpand = [ "Top", "Right", "Bottom", "Left" ], + cssPrefixes = [ "Webkit", "O", "Moz", "ms" ], + + eventsToggle = jQuery.fn.toggle; + +// return a css property mapped to a potentially vendor prefixed property +function vendorPropName( style, name ) { + + // shortcut for names that are not vendor prefixed + if ( name in style ) { + return name; + } + + // check for vendor prefixed names + var capName = name.charAt(0).toUpperCase() + name.slice(1), + origName = name, + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in style ) { + return name; + } + } + + return origName; +} + +function isHidden( elem, el ) { + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); +} + +function showHide( elements, show ) { + var elem, display, + values = [], + index = 0, + length = elements.length; + + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + values[ index ] = jQuery._data( elem, "olddisplay" ); + if ( show ) { + // Reset the inline display of this element to learn if it is + // being hidden by cascaded rules or not + if ( !values[ index ] && elem.style.display === "none" ) { + elem.style.display = ""; + } + + // Set elements which have been overridden with display: none + // in a stylesheet to whatever the default browser style is + // for such an element + if ( elem.style.display === "" && isHidden( elem ) ) { + values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) ); + } + } else { + display = curCSS( elem, "display" ); + + if ( !values[ index ] && display !== "none" ) { + jQuery._data( elem, "olddisplay", display ); + } + } + } + + // Set the display of most of the elements in a second loop + // to avoid the constant reflow + for ( index = 0; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + if ( !show || elem.style.display === "none" || elem.style.display === "" ) { + elem.style.display = show ? values[ index ] || "" : "none"; + } + } + + return elements; +} + +jQuery.fn.extend({ + css: function( name, value ) { + return jQuery.access( this, function( elem, name, value ) { + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + }, + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state, fn2 ) { + var bool = typeof state === "boolean"; + + if ( jQuery.isFunction( state ) && jQuery.isFunction( fn2 ) ) { + return eventsToggle.apply( this, arguments ); + } + + return this.each(function() { + if ( bool ? state : isHidden( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + }); + } +}); + +jQuery.extend({ + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + + } + } + } + }, + + // Exclude the following css properties to add px + cssNumber: { + "fillOpacity": true, + "fontWeight": true, + "lineHeight": true, + "opacity": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: { + // normalize float css property + "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" + }, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = jQuery.camelCase( name ), + style = elem.style; + + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // convert relative number strings (+= or -=) to relative numbers. #7345 + if ( type === "string" && (ret = rrelNum.exec( value )) ) { + value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); + // Fixes bug #9237 + type = "number"; + } + + // Make sure that NaN and null values aren't set. See: #7116 + if ( value == null || type === "number" && isNaN( value ) ) { + return; + } + + // If a number was passed in, add 'px' to the (except for certain CSS properties) + if ( type === "number" && !jQuery.cssNumber[ origName ] ) { + value += "px"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { + // Wrapped to prevent IE from throwing errors when 'invalid' values are provided + // Fixes bug #5509 + try { + style[ name ] = value; + } catch(e) {} + } + + } else { + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, numeric, extra ) { + var val, num, hooks, + origName = jQuery.camelCase( name ); + + // Make sure that we're working with the right name + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name ); + } + + //convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Return, converting to number if forced or a qualifier was provided and val looks numeric + if ( numeric || extra !== undefined ) { + num = parseFloat( val ); + return numeric || jQuery.isNumeric( num ) ? num || 0 : val; + } + return val; + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.call( elem ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; + } +}); + +// NOTE: To any future maintainer, we've window.getComputedStyle +// because jsdom on node.js will break without it. +if ( window.getComputedStyle ) { + curCSS = function( elem, name ) { + var ret, width, minWidth, maxWidth, + computed = window.getComputedStyle( elem, null ), + style = elem.style; + + if ( computed ) { + + // getPropertyValue is only needed for .css('filter') in IE9, see #12537 + ret = computed.getPropertyValue( name ) || computed[ name ]; + + if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right + // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels + // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values + if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret; + }; +} else if ( document.documentElement.currentStyle ) { + curCSS = function( elem, name ) { + var left, rsLeft, + ret = elem.currentStyle && elem.currentStyle[ name ], + style = elem.style; + + // Avoid setting ret to empty string here + // so we don't default to auto + if ( ret == null && style && style[ name ] ) { + ret = style[ name ]; + } + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + // but not position css attributes, as those are proportional to the parent element instead + // and we can't measure the parent instead because it might trigger a "stacking dolls" problem + if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { + + // Remember the original values + left = style.left; + rsLeft = elem.runtimeStyle && elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + if ( rsLeft ) { + elem.runtimeStyle.left = elem.currentStyle.left; + } + style.left = name === "fontSize" ? "1em" : ret; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + if ( rsLeft ) { + elem.runtimeStyle.left = rsLeft; + } + } + + return ret === "" ? "auto" : ret; + }; +} + +function setPositiveNumber( elem, value, subtract ) { + var matches = rnumsplit.exec( value ); + return matches ? + Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : + value; +} + +function augmentWidthOrHeight( elem, name, extra, isBorderBox ) { + var i = extra === ( isBorderBox ? "border" : "content" ) ? + // If we already have the right measurement, avoid augmentation + 4 : + // Otherwise initialize for horizontal or vertical properties + name === "width" ? 1 : 0, + + val = 0; + + for ( ; i < 4; i += 2 ) { + // both box models exclude margin, so add it if we want it + if ( extra === "margin" ) { + // we use jQuery.css instead of curCSS here + // because of the reliableMarginRight CSS hook! + val += jQuery.css( elem, extra + cssExpand[ i ], true ); + } + + // From this point on we use curCSS for maximum performance (relevant in animations) + if ( isBorderBox ) { + // border-box includes padding, so remove it if we want content + if ( extra === "content" ) { + val -= parseFloat( curCSS( elem, "padding" + cssExpand[ i ] ) ) || 0; + } + + // at this point, extra isn't border nor margin, so remove border + if ( extra !== "margin" ) { + val -= parseFloat( curCSS( elem, "border" + cssExpand[ i ] + "Width" ) ) || 0; + } + } else { + // at this point, extra isn't content, so add padding + val += parseFloat( curCSS( elem, "padding" + cssExpand[ i ] ) ) || 0; + + // at this point, extra isn't content nor padding, so add border + if ( extra !== "padding" ) { + val += parseFloat( curCSS( elem, "border" + cssExpand[ i ] + "Width" ) ) || 0; + } + } + } + + return val; +} + +function getWidthOrHeight( elem, name, extra ) { + + // Start with offset property, which is equivalent to the border-box value + var val = name === "width" ? elem.offsetWidth : elem.offsetHeight, + valueIsBorderBox = true, + isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing" ) === "border-box"; + + // some non-html elements return undefined for offsetWidth, so check for null/undefined + // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 + // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 + if ( val <= 0 || val == null ) { + // Fall back to computed then uncomputed css if necessary + val = curCSS( elem, name ); + if ( val < 0 || val == null ) { + val = elem.style[ name ]; + } + + // Computed unit is not pixels. Stop here and return. + if ( rnumnonpx.test(val) ) { + return val; + } + + // we need the check for style in case a browser which returns unreliable values + // for getComputedStyle silently falls back to the reliable elem.style + valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] ); + + // Normalize "", auto, and prepare for extra + val = parseFloat( val ) || 0; + } + + // use the active box-sizing model to add/subtract irrelevant styles + return ( val + + augmentWidthOrHeight( + elem, + name, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox + ) + ) + "px"; +} + + +// Try to determine the default display value of an element +function css_defaultDisplay( nodeName ) { + if ( elemdisplay[ nodeName ] ) { + return elemdisplay[ nodeName ]; + } + + var elem = jQuery( "<" + nodeName + ">" ).appendTo( document.body ), + display = elem.css("display"); + elem.remove(); + + // If the simple way fails, + // get element's real default display by attaching it to a temp iframe + if ( display === "none" || display === "" ) { + // Use the already-created iframe if possible + iframe = document.body.appendChild( + iframe || jQuery.extend( document.createElement("iframe"), { + frameBorder: 0, + width: 0, + height: 0 + }) + ); + + // Create a cacheable copy of the iframe document on first call. + // IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML + // document to it; WebKit & Firefox won't allow reusing the iframe document. + if ( !iframeDoc || !iframe.createElement ) { + iframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document; + iframeDoc.write(""); + iframeDoc.close(); + } + + elem = iframeDoc.body.appendChild( iframeDoc.createElement(nodeName) ); + + display = curCSS( elem, "display" ); + document.body.removeChild( iframe ); + } + + // Store the correct default display + elemdisplay[ nodeName ] = display; + + return display; +} + +jQuery.each([ "height", "width" ], function( i, name ) { + jQuery.cssHooks[ name ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + // certain elements can have dimension info if we invisibly show them + // however, it must have a current display style that would benefit from this + if ( elem.offsetWidth === 0 && rdisplayswap.test( curCSS( elem, "display" ) ) ) { + return jQuery.swap( elem, cssShow, function() { + return getWidthOrHeight( elem, name, extra ); + }); + } else { + return getWidthOrHeight( elem, name, extra ); + } + } + }, + + set: function( elem, value, extra ) { + return setPositiveNumber( elem, value, extra ? + augmentWidthOrHeight( + elem, + name, + extra, + jQuery.support.boxSizing && jQuery.css( elem, "boxSizing" ) === "border-box" + ) : 0 + ); + } + }; +}); + +if ( !jQuery.support.opacity ) { + jQuery.cssHooks.opacity = { + get: function( elem, computed ) { + // IE uses filters for opacity + return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ? + ( 0.01 * parseFloat( RegExp.$1 ) ) + "" : + computed ? "1" : ""; + }, + + set: function( elem, value ) { + var style = elem.style, + currentStyle = elem.currentStyle, + opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "", + filter = currentStyle && currentStyle.filter || style.filter || ""; + + // IE has trouble with opacity if it does not have layout + // Force it by setting the zoom level + style.zoom = 1; + + // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652 + if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" && + style.removeAttribute ) { + + // Setting style.filter to null, "" & " " still leave "filter:" in the cssText + // if "filter:" is present at all, clearType is disabled, we want to avoid this + // style.removeAttribute is IE Only, but so apparently is this code path... + style.removeAttribute( "filter" ); + + // if there there is no filter style applied in a css rule, we are done + if ( currentStyle && !currentStyle.filter ) { + return; + } + } + + // otherwise, set new filter values + style.filter = ralpha.test( filter ) ? + filter.replace( ralpha, opacity ) : + filter + " " + opacity; + } + }; +} + +// These hooks cannot be added until DOM ready because the support test +// for it is not run until after DOM ready +jQuery(function() { + if ( !jQuery.support.reliableMarginRight ) { + jQuery.cssHooks.marginRight = { + get: function( elem, computed ) { + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + // Work around by temporarily setting element display to inline-block + return jQuery.swap( elem, { "display": "inline-block" }, function() { + if ( computed ) { + return curCSS( elem, "marginRight" ); + } + }); + } + }; + } + + // Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084 + // getComputedStyle returns percent when specified for top/left/bottom/right + // rather than make the css module depend on the offset module, we just check for it here + if ( !jQuery.support.pixelPosition && jQuery.fn.position ) { + jQuery.each( [ "top", "left" ], function( i, prop ) { + jQuery.cssHooks[ prop ] = { + get: function( elem, computed ) { + if ( computed ) { + var ret = curCSS( elem, prop ); + // if curCSS returns percentage, fallback to offset + return rnumnonpx.test( ret ) ? jQuery( elem ).position()[ prop ] + "px" : ret; + } + } + }; + }); + } + +}); + +if ( jQuery.expr && jQuery.expr.filters ) { + jQuery.expr.filters.hidden = function( elem ) { + return ( elem.offsetWidth === 0 && elem.offsetHeight === 0 ) || (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || curCSS( elem, "display" )) === "none"); + }; + + jQuery.expr.filters.visible = function( elem ) { + return !jQuery.expr.filters.hidden( elem ); + }; +} + +// These hooks are used by animate to expand properties +jQuery.each({ + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i, + + // assumes a single number if not a string + parts = typeof value === "string" ? value.split(" ") : [ value ], + expanded = {}; + + for ( i = 0; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + } + + return expanded; + } + }; + + if ( !rmargin.test( prefix ) ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +}); +var r20 = /%20/g, + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rinput = /^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i, + rselectTextarea = /^(?:select|textarea)/i; + +jQuery.fn.extend({ + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map(function(){ + return this.elements ? jQuery.makeArray( this.elements ) : this; + }) + .filter(function(){ + return this.name && !this.disabled && + ( this.checked || rselectTextarea.test( this.nodeName ) || + rinput.test( this.type ) ); + }) + .map(function( i, elem ){ + var val = jQuery( this ).val(); + + return val == null ? + null : + jQuery.isArray( val ) ? + jQuery.map( val, function( val, i ){ + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + }) : + { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + }).get(); + } +}); + +//Serialize an array of form elements or a set of +//key/values into a query string +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, value ) { + // If value is a function, invoke it and return its value + value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value ); + s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value ); + }; + + // Set traditional to true for jQuery <= 1.3.2 behavior. + if ( traditional === undefined ) { + traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional; + } + + // If an array was passed in, assume that it is an array of form elements. + if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + }); + + } else { + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + // Return the resulting serialization + return s.join( "&" ).replace( r20, "+" ); +}; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( jQuery.isArray( obj ) ) { + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + // If array item is non-scalar (array or object), encode its + // numeric index to resolve deserialization ambiguity issues. + // Note that rack (as of 1.0.0) can't currently deserialize + // nested arrays properly, and attempting to do so may cause + // a server error. Possible fixes are to modify rack's + // deserialization algorithm or to provide an option or flag + // to force array serialization to be shallow. + buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add ); + } + }); + + } else if ( !traditional && jQuery.type( obj ) === "object" ) { + // Serialize object item. + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + // Serialize scalar item. + add( prefix, obj ); + } +} +var + // Document location + ajaxLocParts, + ajaxLocation, + + rhash = /#.*$/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL + // #7653, #8125, #8152: local protocol detection + rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\//, + rquery = /\?/, + rscript = /)<[^<]*)*<\/script>/gi, + rts = /([?&])_=[^&]*/, + rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/, + + // Keep a copy of the old load method + _load = jQuery.fn.load, + + /* Prefilters + * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) + * 2) These are called: + * - BEFORE asking for a transport + * - AFTER param serialization (s.data is a string if s.processData is true) + * 3) key is the dataType + * 4) the catchall symbol "*" can be used + * 5) execution will start with transport dataType and THEN continue down to "*" if needed + */ + prefilters = {}, + + /* Transports bindings + * 1) key is the dataType + * 2) the catchall symbol "*" can be used + * 3) selection will start with transport dataType and THEN go to "*" if needed + */ + transports = {}, + + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression + allTypes = ["*/"] + ["*"]; + +// #8138, IE may throw an exception when accessing +// a field from window.location if document.domain has been set +try { + ajaxLocation = location.href; +} catch( e ) { + // Use the href attribute of an A element + // since IE will modify it given document.location + ajaxLocation = document.createElement( "a" ); + ajaxLocation.href = ""; + ajaxLocation = ajaxLocation.href; +} + +// Segment location into parts +ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || []; + +// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport +function addToPrefiltersOrTransports( structure ) { + + // dataTypeExpression is optional and defaults to "*" + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + var dataType, list, placeBefore, + dataTypes = dataTypeExpression.toLowerCase().split( core_rspace ), + i = 0, + length = dataTypes.length; + + if ( jQuery.isFunction( func ) ) { + // For each dataType in the dataTypeExpression + for ( ; i < length; i++ ) { + dataType = dataTypes[ i ]; + // We control if we're asked to add before + // any existing element + placeBefore = /^\+/.test( dataType ); + if ( placeBefore ) { + dataType = dataType.substr( 1 ) || "*"; + } + list = structure[ dataType ] = structure[ dataType ] || []; + // then we add to the structure accordingly + list[ placeBefore ? "unshift" : "push" ]( func ); + } + } + }; +} + +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR, + dataType /* internal */, inspected /* internal */ ) { + + dataType = dataType || options.dataTypes[ 0 ]; + inspected = inspected || {}; + + inspected[ dataType ] = true; + + var selection, + list = structure[ dataType ], + i = 0, + length = list ? list.length : 0, + executeOnly = ( structure === prefilters ); + + for ( ; i < length && ( executeOnly || !selection ); i++ ) { + selection = list[ i ]( options, originalOptions, jqXHR ); + // If we got redirected to another dataType + // we try there if executing only and not done already + if ( typeof selection === "string" ) { + if ( !executeOnly || inspected[ selection ] ) { + selection = undefined; + } else { + options.dataTypes.unshift( selection ); + selection = inspectPrefiltersOrTransports( + structure, options, originalOptions, jqXHR, selection, inspected ); + } + } + } + // If we're only executing or nothing was selected + // we try the catchall dataType if not done already + if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) { + selection = inspectPrefiltersOrTransports( + structure, options, originalOptions, jqXHR, "*", inspected ); + } + // unnecessary when only executing (prefilters) + // but it'll be ignored by the caller in that case + return selection; +} + +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes #9887 +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } +} + +jQuery.fn.load = function( url, params, callback ) { + if ( typeof url !== "string" && _load ) { + return _load.apply( this, arguments ); + } + + // Don't do a request if no elements are being requested + if ( !this.length ) { + return this; + } + + var selector, type, response, + self = this, + off = url.indexOf(" "); + + if ( off >= 0 ) { + selector = url.slice( off, url.length ); + url = url.slice( 0, off ); + } + + // If it's a function + if ( jQuery.isFunction( params ) ) { + + // We assume that it's the callback + callback = params; + params = undefined; + + // Otherwise, build a param string + } else if ( params && typeof params === "object" ) { + type = "POST"; + } + + // Request the remote document + jQuery.ajax({ + url: url, + + // if "type" variable is undefined, then "GET" method will be used + type: type, + dataType: "html", + data: params, + complete: function( jqXHR, status ) { + if ( callback ) { + self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] ); + } + } + }).done(function( responseText ) { + + // Save response for use in complete callback + response = arguments; + + // See if a selector was specified + self.html( selector ? + + // Create a dummy div to hold the results + jQuery("
") + + // inject the contents of the document in, removing the scripts + // to avoid any 'Permission Denied' errors in IE + .append( responseText.replace( rscript, "" ) ) + + // Locate the specified elements + .find( selector ) : + + // If not, just inject the full result + responseText ); + + }); + + return this; +}; + +// Attach a bunch of functions for handling common AJAX events +jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split( " " ), function( i, o ){ + jQuery.fn[ o ] = function( f ){ + return this.on( o, f ); + }; +}); + +jQuery.each( [ "get", "post" ], function( i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + // shift arguments if data argument was omitted + if ( jQuery.isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + return jQuery.ajax({ + type: method, + url: url, + data: data, + success: callback, + dataType: type + }); + }; +}); + +jQuery.extend({ + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + }, + + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + if ( settings ) { + // Building a settings object + ajaxExtend( target, jQuery.ajaxSettings ); + } else { + // Extending ajaxSettings + settings = target; + target = jQuery.ajaxSettings; + } + ajaxExtend( target, settings ); + return target; + }, + + ajaxSettings: { + url: ajaxLocation, + isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ), + global: true, + type: "GET", + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + processData: true, + async: true, + /* + timeout: 0, + data: null, + dataType: null, + username: null, + password: null, + cache: null, + throws: false, + traditional: false, + headers: {}, + */ + + accepts: { + xml: "application/xml, text/xml", + html: "text/html", + text: "text/plain", + json: "application/json, text/javascript", + "*": allTypes + }, + + contents: { + xml: /xml/, + html: /html/, + json: /json/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText" + }, + + // List of data converters + // 1) key format is "source_type destination_type" (a single space in-between) + // 2) the catchall symbol "*" can be used for source_type + converters: { + + // Convert anything to text + "* text": window.String, + + // Text to html (true = no transformation) + "text html": true, + + // Evaluate text as a json expression + "text json": jQuery.parseJSON, + + // Parse text as xml + "text xml": jQuery.parseXML + }, + + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + context: true, + url: true + } + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + // Main method + ajax: function( url, options ) { + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + var // ifModified key + ifModifiedKey, + // Response headers + responseHeadersString, + responseHeaders, + // transport + transport, + // timeout handle + timeoutTimer, + // Cross-domain detection vars + parts, + // To know if global events are to be dispatched + fireGlobals, + // Loop variable + i, + // Create the final options object + s = jQuery.ajaxSetup( {}, options ), + // Callbacks context + callbackContext = s.context || s, + // Context for global events + // It's the callbackContext if one was provided in the options + // and if it's a DOM node or a jQuery collection + globalEventContext = callbackContext !== s && + ( callbackContext.nodeType || callbackContext instanceof jQuery ) ? + jQuery( callbackContext ) : jQuery.event, + // Deferreds + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + // Status-dependent callbacks + statusCode = s.statusCode || {}, + // Headers (they are sent all at once) + requestHeaders = {}, + requestHeadersNames = {}, + // The jqXHR state + state = 0, + // Default abort message + strAbort = "canceled", + // Fake xhr + jqXHR = { + + readyState: 0, + + // Caches the header + setRequestHeader: function( name, value ) { + if ( !state ) { + var lname = name.toLowerCase(); + name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + // Raw string + getAllResponseHeaders: function() { + return state === 2 ? responseHeadersString : null; + }, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + var match; + if ( state === 2 ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[1].toLowerCase() ] = match[ 2 ]; + } + } + match = responseHeaders[ key.toLowerCase() ]; + } + return match === undefined ? null : match; + }, + + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( !state ) { + s.mimeType = type; + } + return this; + }, + + // Cancel the request + abort: function( statusText ) { + statusText = statusText || strAbort; + if ( transport ) { + transport.abort( statusText ); + } + done( 0, statusText ); + return this; + } + }; + + // Callback for when everything is done + // It is defined here because jslint complains if it is declared + // at the end of the function (which would be more logical and readable) + function done( status, nativeStatusText, responses, headers ) { + var isSuccess, success, error, response, modified, + statusText = nativeStatusText; + + // Called once + if ( state === 2 ) { + return; + } + + // State is "done" now + state = 2; + + // Clear timeout if it exists + if ( timeoutTimer ) { + clearTimeout( timeoutTimer ); + } + + // Dereference transport for early garbage collection + // (no matter how long the jqXHR object will be used) + transport = undefined; + + // Cache response headers + responseHeadersString = headers || ""; + + // Set readyState + jqXHR.readyState = status > 0 ? 4 : 0; + + // Get response data + if ( responses ) { + response = ajaxHandleResponses( s, jqXHR, responses ); + } + + // If successful, handle type chaining + if ( status >= 200 && status < 300 || status === 304 ) { + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + + modified = jqXHR.getResponseHeader("Last-Modified"); + if ( modified ) { + jQuery.lastModified[ ifModifiedKey ] = modified; + } + modified = jqXHR.getResponseHeader("Etag"); + if ( modified ) { + jQuery.etag[ ifModifiedKey ] = modified; + } + } + + // If not modified + if ( status === 304 ) { + + statusText = "notmodified"; + isSuccess = true; + + // If we have data + } else { + + isSuccess = ajaxConvert( s, response ); + statusText = isSuccess.state; + success = isSuccess.data; + error = isSuccess.error; + isSuccess = !error; + } + } else { + // We extract error from statusText + // then normalize statusText and status for non-aborts + error = statusText; + if ( !statusText || status ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + // Set data for the fake xhr object + jqXHR.status = status; + jqXHR.statusText = ( nativeStatusText || statusText ) + ""; + + // Success/Error + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + // Status-dependent callbacks + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ), + [ jqXHR, s, isSuccess ? success : error ] ); + } + + // Complete + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + // Handle the global AJAX counter + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + // Attach deferreds + deferred.promise( jqXHR ); + jqXHR.success = jqXHR.done; + jqXHR.error = jqXHR.fail; + jqXHR.complete = completeDeferred.add; + + // Status-dependent callbacks + jqXHR.statusCode = function( map ) { + if ( map ) { + var tmp; + if ( state < 2 ) { + for ( tmp in map ) { + statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ]; + } + } else { + tmp = map[ jqXHR.status ]; + jqXHR.always( tmp ); + } + } + return this; + }; + + // Remove hash character (#7531: and string promotion) + // Add protocol if not provided (#5866: IE7 issue with protocol-less urls) + // We also use the url parameter if available + s.url = ( ( url || s.url ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" ); + + // Extract dataTypes list + s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( core_rspace ); + + // A cross-domain request is in order when we have a protocol:host:port mismatch + if ( s.crossDomain == null ) { + parts = rurl.exec( s.url.toLowerCase() ); + s.crossDomain = !!( parts && + ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] || + ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) != + ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) ) + ); + } + + // Convert data if not already a string + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + // Apply prefilters + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefilter, stop there + if ( state === 2 ) { + return jqXHR; + } + + // We can fire global events as of now if asked to + fireGlobals = s.global; + + // Uppercase the type + s.type = s.type.toUpperCase(); + + // Determine if request has content + s.hasContent = !rnoContent.test( s.type ); + + // Watch for a new set of requests + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + // More options handling for requests with no content + if ( !s.hasContent ) { + + // If data is available, append data to url + if ( s.data ) { + s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data; + // #9682: remove data so that it's not used in an eventual retry + delete s.data; + } + + // Get ifModifiedKey before adding the anti-cache parameter + ifModifiedKey = s.url; + + // Add anti-cache in url if needed + if ( s.cache === false ) { + + var ts = jQuery.now(), + // try replacing _= if it is there + ret = s.url.replace( rts, "$1_=" + ts ); + + // if nothing was replaced, add timestamp to the end + s.url = ret + ( ( ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" ); + } + } + + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + ifModifiedKey = ifModifiedKey || s.url; + if ( jQuery.lastModified[ ifModifiedKey ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ ifModifiedKey ] ); + } + if ( jQuery.etag[ ifModifiedKey ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ ifModifiedKey ] ); + } + } + + // Set the Accepts header for the server, depending on the dataType + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ? + s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + // Check for headers option + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) { + // Abort if not done already and return + return jqXHR.abort(); + + } + + // aborting is no longer a cancellation + strAbort = "abort"; + + // Install callbacks on deferreds + for ( i in { success: 1, error: 1, complete: 1 } ) { + jqXHR[ i ]( s[ i ] ); + } + + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = setTimeout( function(){ + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + state = 1; + transport.send( requestHeaders, done ); + } catch (e) { + // Propagate exception as error if not done + if ( state < 2 ) { + done( -1, e ); + // Simply rethrow otherwise + } else { + throw e; + } + } + } + + return jqXHR; + }, + + // Counter for holding the number of active queries + active: 0, + + // Last-Modified header cache for next request + lastModified: {}, + etag: {} + +}); + +/* Handles responses to an ajax request: + * - sets all responseXXX fields accordingly + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + + var ct, type, finalDataType, firstDataType, + contents = s.contents, + dataTypes = s.dataTypes, + responseFields = s.responseFields; + + // Fill responseXXX fields + for ( type in responseFields ) { + if ( type in responses ) { + jqXHR[ responseFields[type] ] = responses[ type ]; + } + } + + // Remove auto dataType and get content-type in the process + while( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "content-type" ); + } + } + + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + // Or just use first one + finalDataType = finalDataType || firstDataType; + } + + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +// Chain conversions given the request and the original response +function ajaxConvert( s, response ) { + + var conv, conv2, current, tmp, + // Work with a copy of dataTypes in case we need to modify it for conversion + dataTypes = s.dataTypes.slice(), + prev = dataTypes[ 0 ], + converters = {}, + i = 0; + + // Apply the dataFilter if provided + if ( s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + // Create converters map with lowercased keys + if ( dataTypes[ 1 ] ) { + for ( conv in s.converters ) { + converters[ conv.toLowerCase() ] = s.converters[ conv ]; + } + } + + // Convert to each sequential dataType, tolerating list modification + for ( ; (current = dataTypes[++i]); ) { + + // There's only work to do if current dataType is non-auto + if ( current !== "*" ) { + + // Convert response if prev dataType is non-auto and differs from current + if ( prev !== "*" && prev !== current ) { + + // Seek a direct converter + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; + + // If none found, seek a pair + if ( !conv ) { + for ( conv2 in converters ) { + + // If conv2 outputs current + tmp = conv2.split(" "); + if ( tmp[ 1 ] === current ) { + + // If prev can be converted to accepted input + conv = converters[ prev + " " + tmp[ 0 ] ] || + converters[ "* " + tmp[ 0 ] ]; + if ( conv ) { + // Condense equivalence converters + if ( conv === true ) { + conv = converters[ conv2 ]; + + // Otherwise, insert the intermediate dataType + } else if ( converters[ conv2 ] !== true ) { + current = tmp[ 0 ]; + dataTypes.splice( i--, 0, current ); + } + + break; + } + } + } + } + + // Apply converter (if not an equivalence) + if ( conv !== true ) { + + // Unless errors are allowed to bubble, catch and return them + if ( conv && s["throws"] ) { + response = conv( response ); + } else { + try { + response = conv( response ); + } catch ( e ) { + return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current }; + } + } + } + } + + // Update prev for next iteration + prev = current; + } + } + + return { state: "success", data: response }; +} +var oldCallbacks = [], + rquestion = /\?/, + rjsonp = /(=)\?(?=&|$)|\?\?/, + nonce = jQuery.now(); + +// Default jsonp settings +jQuery.ajaxSetup({ + jsonp: "callback", + jsonpCallback: function() { + var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) ); + this[ callback ] = true; + return callback; + } +}); + +// Detect, normalize options and install callbacks for jsonp requests +jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { + + var callbackName, overwritten, responseContainer, + data = s.data, + url = s.url, + hasCallback = s.jsonp !== false, + replaceInUrl = hasCallback && rjsonp.test( url ), + replaceInData = hasCallback && !replaceInUrl && typeof data === "string" && + !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && + rjsonp.test( data ); + + // Handle iff the expected data type is "jsonp" or we have a parameter to set + if ( s.dataTypes[ 0 ] === "jsonp" || replaceInUrl || replaceInData ) { + + // Get callback name, remembering preexisting value associated with it + callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ? + s.jsonpCallback() : + s.jsonpCallback; + overwritten = window[ callbackName ]; + + // Insert callback into url or form data + if ( replaceInUrl ) { + s.url = url.replace( rjsonp, "$1" + callbackName ); + } else if ( replaceInData ) { + s.data = data.replace( rjsonp, "$1" + callbackName ); + } else if ( hasCallback ) { + s.url += ( rquestion.test( url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName; + } + + // Use data converter to retrieve json after script execution + s.converters["script json"] = function() { + if ( !responseContainer ) { + jQuery.error( callbackName + " was not called" ); + } + return responseContainer[ 0 ]; + }; + + // force json dataType + s.dataTypes[ 0 ] = "json"; + + // Install callback + window[ callbackName ] = function() { + responseContainer = arguments; + }; + + // Clean-up function (fires after converters) + jqXHR.always(function() { + // Restore preexisting value + window[ callbackName ] = overwritten; + + // Save back as free + if ( s[ callbackName ] ) { + // make sure that re-using the options doesn't screw things around + s.jsonpCallback = originalSettings.jsonpCallback; + + // save the callback name for future use + oldCallbacks.push( callbackName ); + } + + // Call if it was a function and we have a response + if ( responseContainer && jQuery.isFunction( overwritten ) ) { + overwritten( responseContainer[ 0 ] ); + } + + responseContainer = overwritten = undefined; + }); + + // Delegate to script + return "script"; + } +}); +// Install script dataType +jQuery.ajaxSetup({ + accepts: { + script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" + }, + contents: { + script: /javascript|ecmascript/ + }, + converters: { + "text script": function( text ) { + jQuery.globalEval( text ); + return text; + } + } +}); + +// Handle cache's special case and global +jQuery.ajaxPrefilter( "script", function( s ) { + if ( s.cache === undefined ) { + s.cache = false; + } + if ( s.crossDomain ) { + s.type = "GET"; + s.global = false; + } +}); + +// Bind script tag hack transport +jQuery.ajaxTransport( "script", function(s) { + + // This transport only deals with cross domain requests + if ( s.crossDomain ) { + + var script, + head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement; + + return { + + send: function( _, callback ) { + + script = document.createElement( "script" ); + + script.async = "async"; + + if ( s.scriptCharset ) { + script.charset = s.scriptCharset; + } + + script.src = s.url; + + // Attach handlers for all browsers + script.onload = script.onreadystatechange = function( _, isAbort ) { + + if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) { + + // Handle memory leak in IE + script.onload = script.onreadystatechange = null; + + // Remove the script + if ( head && script.parentNode ) { + head.removeChild( script ); + } + + // Dereference the script + script = undefined; + + // Callback if not abort + if ( !isAbort ) { + callback( 200, "success" ); + } + } + }; + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709 and #4378). + head.insertBefore( script, head.firstChild ); + }, + + abort: function() { + if ( script ) { + script.onload( 0, 1 ); + } + } + }; + } +}); +var xhrCallbacks, + // #5280: Internet Explorer will keep connections alive if we don't abort on unload + xhrOnUnloadAbort = window.ActiveXObject ? function() { + // Abort all pending requests + for ( var key in xhrCallbacks ) { + xhrCallbacks[ key ]( 0, 1 ); + } + } : false, + xhrId = 0; + +// Functions to create xhrs +function createStandardXHR() { + try { + return new window.XMLHttpRequest(); + } catch( e ) {} +} + +function createActiveXHR() { + try { + return new window.ActiveXObject( "Microsoft.XMLHTTP" ); + } catch( e ) {} +} + +// Create the request object +// (This is still attached to ajaxSettings for backward compatibility) +jQuery.ajaxSettings.xhr = window.ActiveXObject ? + /* Microsoft failed to properly + * implement the XMLHttpRequest in IE7 (can't request local files), + * so we use the ActiveXObject when it is available + * Additionally XMLHttpRequest can be disabled in IE7/IE8 so + * we need a fallback. + */ + function() { + return !this.isLocal && createStandardXHR() || createActiveXHR(); + } : + // For all other browsers, use the standard XMLHttpRequest object + createStandardXHR; + +// Determine support properties +(function( xhr ) { + jQuery.extend( jQuery.support, { + ajax: !!xhr, + cors: !!xhr && ( "withCredentials" in xhr ) + }); +})( jQuery.ajaxSettings.xhr() ); + +// Create transport if the browser can provide an xhr +if ( jQuery.support.ajax ) { + + jQuery.ajaxTransport(function( s ) { + // Cross domain only allowed if supported through XMLHttpRequest + if ( !s.crossDomain || jQuery.support.cors ) { + + var callback; + + return { + send: function( headers, complete ) { + + // Get a new xhr + var handle, i, + xhr = s.xhr(); + + // Open the socket + // Passing null username, generates a login popup on Opera (#2865) + if ( s.username ) { + xhr.open( s.type, s.url, s.async, s.username, s.password ); + } else { + xhr.open( s.type, s.url, s.async ); + } + + // Apply custom fields if provided + if ( s.xhrFields ) { + for ( i in s.xhrFields ) { + xhr[ i ] = s.xhrFields[ i ]; + } + } + + // Override mime type if needed + if ( s.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( s.mimeType ); + } + + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !s.crossDomain && !headers["X-Requested-With"] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } + + // Need an extra try/catch for cross domain requests in Firefox 3 + try { + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + } catch( _ ) {} + + // Do send the request + // This may raise an exception which is actually + // handled in jQuery.ajax (so no try/catch here) + xhr.send( ( s.hasContent && s.data ) || null ); + + // Listener + callback = function( _, isAbort ) { + + var status, + statusText, + responseHeaders, + responses, + xml; + + // Firefox throws exceptions when accessing properties + // of an xhr when a network error occurred + // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE) + try { + + // Was never called and is aborted or complete + if ( callback && ( isAbort || xhr.readyState === 4 ) ) { + + // Only called once + callback = undefined; + + // Do not keep as active anymore + if ( handle ) { + xhr.onreadystatechange = jQuery.noop; + if ( xhrOnUnloadAbort ) { + delete xhrCallbacks[ handle ]; + } + } + + // If it's an abort + if ( isAbort ) { + // Abort it manually if needed + if ( xhr.readyState !== 4 ) { + xhr.abort(); + } + } else { + status = xhr.status; + responseHeaders = xhr.getAllResponseHeaders(); + responses = {}; + xml = xhr.responseXML; + + // Construct response list + if ( xml && xml.documentElement /* #4958 */ ) { + responses.xml = xml; + } + + // When requesting binary data, IE6-9 will throw an exception + // on any attempt to access responseText (#11426) + try { + responses.text = xhr.responseText; + } catch( e ) { + } + + // Firefox throws an exception when accessing + // statusText for faulty cross-domain requests + try { + statusText = xhr.statusText; + } catch( e ) { + // We normalize with Webkit giving an empty statusText + statusText = ""; + } + + // Filter status for non standard behaviors + + // If the request is local and we have data: assume a success + // (success with no data won't get notified, that's the best we + // can do given current implementations) + if ( !status && s.isLocal && !s.crossDomain ) { + status = responses.text ? 200 : 404; + // IE - #1450: sometimes returns 1223 when it should be 204 + } else if ( status === 1223 ) { + status = 204; + } + } + } + } catch( firefoxAccessException ) { + if ( !isAbort ) { + complete( -1, firefoxAccessException ); + } + } + + // Call complete if needed + if ( responses ) { + complete( status, statusText, responses, responseHeaders ); + } + }; + + if ( !s.async ) { + // if we're in sync mode we fire the callback + callback(); + } else if ( xhr.readyState === 4 ) { + // (IE6 & IE7) if it's in cache and has been + // retrieved directly we need to fire the callback + setTimeout( callback, 0 ); + } else { + handle = ++xhrId; + if ( xhrOnUnloadAbort ) { + // Create the active xhrs callbacks list if needed + // and attach the unload handler + if ( !xhrCallbacks ) { + xhrCallbacks = {}; + jQuery( window ).unload( xhrOnUnloadAbort ); + } + // Add to list of active xhrs callbacks + xhrCallbacks[ handle ] = callback; + } + xhr.onreadystatechange = callback; + } + }, + + abort: function() { + if ( callback ) { + callback(0,1); + } + } + }; + } + }); +} +var fxNow, timerId, + rfxtypes = /^(?:toggle|show|hide)$/, + rfxnum = new RegExp( "^(?:([-+])=|)(" + core_pnum + ")([a-z%]*)$", "i" ), + rrun = /queueHooks$/, + animationPrefilters = [ defaultPrefilter ], + tweeners = { + "*": [function( prop, value ) { + var end, unit, + tween = this.createTween( prop, value ), + parts = rfxnum.exec( value ), + target = tween.cur(), + start = +target || 0, + scale = 1, + maxIterations = 20; + + if ( parts ) { + end = +parts[2]; + unit = parts[3] || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + + // We need to compute starting value + if ( unit !== "px" && start ) { + // Iteratively approximate from a nonzero starting point + // Prefer the current property, because this process will be trivial if it uses the same units + // Fallback to end or a simple constant + start = jQuery.css( tween.elem, prop, true ) || end || 1; + + do { + // If previous iteration zeroed out, double until we get *something* + // Use a string for doubling factor so we don't accidentally see scale as unchanged below + scale = scale || ".5"; + + // Adjust and apply + start = start / scale; + jQuery.style( tween.elem, prop, start + unit ); + + // Update scale, tolerating zero or NaN from tween.cur() + // And breaking the loop if scale is unchanged or perfect, or if we've just had enough + } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations ); + } + + tween.unit = unit; + tween.start = start; + // If a +=/-= token was provided, we're doing a relative animation + tween.end = parts[1] ? start + ( parts[1] + 1 ) * end : end; + } + return tween; + }] + }; + +// Animations created synchronously will run synchronously +function createFxNow() { + setTimeout(function() { + fxNow = undefined; + }, 0 ); + return ( fxNow = jQuery.now() ); +} + +function createTweens( animation, props ) { + jQuery.each( props, function( prop, value ) { + var collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( collection[ index ].call( animation, prop, value ) ) { + + // we're done with this property + return; + } + } + }); +} + +function Animation( elem, properties, options ) { + var result, + index = 0, + tweenerIndex = 0, + length = animationPrefilters.length, + deferred = jQuery.Deferred().always( function() { + // don't match elem in the :animated selector + delete tick.elem; + }), + tick = function() { + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497) + temp = remaining / animation.duration || 0, + percent = 1 - temp, + index = 0, + length = animation.tweens.length; + + for ( ; index < length ; index++ ) { + animation.tweens[ index ].run( percent ); + } + + deferred.notifyWith( elem, [ animation, percent, remaining ]); + + if ( percent < 1 && length ) { + return remaining; + } else { + deferred.resolveWith( elem, [ animation ] ); + return false; + } + }, + animation = deferred.promise({ + elem: elem, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( true, { specialEasing: {} }, options ), + originalProperties: properties, + originalOptions: options, + startTime: fxNow || createFxNow(), + duration: options.duration, + tweens: [], + createTween: function( prop, end, easing ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + stop: function( gotoEnd ) { + var index = 0, + // if we are going to the end, we want to run all the tweens + // otherwise we skip this part + length = gotoEnd ? animation.tweens.length : 0; + + for ( ; index < length ; index++ ) { + animation.tweens[ index ].run( 1 ); + } + + // resolve when we played the last frame + // otherwise, reject + if ( gotoEnd ) { + deferred.resolveWith( elem, [ animation, gotoEnd ] ); + } else { + deferred.rejectWith( elem, [ animation, gotoEnd ] ); + } + return this; + } + }), + props = animation.props; + + propFilter( props, animation.opts.specialEasing ); + + for ( ; index < length ; index++ ) { + result = animationPrefilters[ index ].call( animation, elem, props, animation.opts ); + if ( result ) { + return result; + } + } + + createTweens( animation, props ); + + if ( jQuery.isFunction( animation.opts.start ) ) { + animation.opts.start.call( elem, animation ); + } + + jQuery.fx.timer( + jQuery.extend( tick, { + anim: animation, + queue: animation.opts.queue, + elem: elem + }) + ); + + // attach callbacks from options + return animation.progress( animation.opts.progress ) + .done( animation.opts.done, animation.opts.complete ) + .fail( animation.opts.fail ) + .always( animation.opts.always ); +} + +function propFilter( props, specialEasing ) { + var index, name, easing, value, hooks; + + // camelCase, specialEasing and expand cssHook pass + for ( index in props ) { + name = jQuery.camelCase( index ); + easing = specialEasing[ name ]; + value = props[ index ]; + if ( jQuery.isArray( value ) ) { + easing = value[ 1 ]; + value = props[ index ] = value[ 0 ]; + } + + if ( index !== name ) { + props[ name ] = value; + delete props[ index ]; + } + + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + value = hooks.expand( value ); + delete props[ name ]; + + // not quite $.extend, this wont overwrite keys already present. + // also - reusing 'index' from above because we have the correct "name" + for ( index in value ) { + if ( !( index in props ) ) { + props[ index ] = value[ index ]; + specialEasing[ index ] = easing; + } + } + } else { + specialEasing[ name ] = easing; + } + } +} + +jQuery.Animation = jQuery.extend( Animation, { + + tweener: function( props, callback ) { + if ( jQuery.isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.split(" "); + } + + var prop, + index = 0, + length = props.length; + + for ( ; index < length ; index++ ) { + prop = props[ index ]; + tweeners[ prop ] = tweeners[ prop ] || []; + tweeners[ prop ].unshift( callback ); + } + }, + + prefilter: function( callback, prepend ) { + if ( prepend ) { + animationPrefilters.unshift( callback ); + } else { + animationPrefilters.push( callback ); + } + } +}); + +function defaultPrefilter( elem, props, opts ) { + var index, prop, value, length, dataShow, toggle, tween, hooks, oldfire, + anim = this, + style = elem.style, + orig = {}, + handled = [], + hidden = elem.nodeType && isHidden( elem ); + + // handle queue: false promises + if ( !opts.queue ) { + hooks = jQuery._queueHooks( elem, "fx" ); + if ( hooks.unqueued == null ) { + hooks.unqueued = 0; + oldfire = hooks.empty.fire; + hooks.empty.fire = function() { + if ( !hooks.unqueued ) { + oldfire(); + } + }; + } + hooks.unqueued++; + + anim.always(function() { + // doing this makes sure that the complete handler will be called + // before this completes + anim.always(function() { + hooks.unqueued--; + if ( !jQuery.queue( elem, "fx" ).length ) { + hooks.empty.fire(); + } + }); + }); + } + + // height/width overflow pass + if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) { + // Make sure that nothing sneaks out + // Record all 3 overflow attributes because IE does not + // change the overflow attribute when overflowX and + // overflowY are set to the same value + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; + + // Set display property to inline-block for height/width + // animations on inline elements that are having width/height animated + if ( jQuery.css( elem, "display" ) === "inline" && + jQuery.css( elem, "float" ) === "none" ) { + + // inline-level elements accept inline-block; + // block-level elements need to be inline with layout + if ( !jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay( elem.nodeName ) === "inline" ) { + style.display = "inline-block"; + + } else { + style.zoom = 1; + } + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + if ( !jQuery.support.shrinkWrapBlocks ) { + anim.done(function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + }); + } + } + + + // show/hide pass + for ( index in props ) { + value = props[ index ]; + if ( rfxtypes.exec( value ) ) { + delete props[ index ]; + toggle = toggle || value === "toggle"; + if ( value === ( hidden ? "hide" : "show" ) ) { + continue; + } + handled.push( index ); + } + } + + length = handled.length; + if ( length ) { + dataShow = jQuery._data( elem, "fxshow" ) || jQuery._data( elem, "fxshow", {} ); + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; + } + + // store state if its toggle - enables .stop().toggle() to "reverse" + if ( toggle ) { + dataShow.hidden = !hidden; + } + if ( hidden ) { + jQuery( elem ).show(); + } else { + anim.done(function() { + jQuery( elem ).hide(); + }); + } + anim.done(function() { + var prop; + jQuery.removeData( elem, "fxshow", true ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + }); + for ( index = 0 ; index < length ; index++ ) { + prop = handled[ index ]; + tween = anim.createTween( prop, hidden ? dataShow[ prop ] : 0 ); + orig[ prop ] = dataShow[ prop ] || jQuery.style( elem, prop ); + + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = tween.start; + if ( hidden ) { + tween.end = tween.start; + tween.start = prop === "width" || prop === "height" ? 1 : 0; + } + } + } + } +} + +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || "swing"; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; + + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); + }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; + + if ( this.options.duration ) { + this.pos = eased = jQuery.easing[ this.easing ]( + percent, this.options.duration * percent, 0, 1, this.options.duration + ); + } else { + this.pos = eased = percent; + } + this.now = ( this.end - this.start ) * eased + this.start; + + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; + +Tween.prototype.init.prototype = Tween.prototype; + +Tween.propHooks = { + _default: { + get: function( tween ) { + var result; + + if ( tween.elem[ tween.prop ] != null && + (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) { + return tween.elem[ tween.prop ]; + } + + // passing any value as a 4th parameter to .css will automatically + // attempt a parseFloat and fallback to a string if the parse fails + // so, simple values such as "10px" are parsed to Float. + // complex values such as "rotate(1rad)" are returned as is. + result = jQuery.css( tween.elem, tween.prop, false, "" ); + // Empty strings, null, undefined and "auto" are converted to 0. + return !result || result === "auto" ? 0 : result; + }, + set: function( tween ) { + // use step hook for back compat - use cssHook if its there - use .style if its + // available and use plain properties where available + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; + } + } + } +}; + +// Remove in 2.0 - this supports IE8's panic based approach +// to setting things on disconnected nodes + +Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { + set: function( tween ) { + if ( tween.elem.nodeType && tween.elem.parentNode ) { + tween.elem[ tween.prop ] = tween.now; + } + } +}; + +jQuery.each([ "toggle", "show", "hide" ], function( i, name ) { + var cssFn = jQuery.fn[ name ]; + jQuery.fn[ name ] = function( speed, easing, callback ) { + return speed == null || typeof speed === "boolean" || + // special check for .toggle( handler, handler, ... ) + ( !i && jQuery.isFunction( speed ) && jQuery.isFunction( easing ) ) ? + cssFn.apply( this, arguments ) : + this.animate( genFx( name, true ), speed, easing, callback ); + }; +}); + +jQuery.fn.extend({ + fadeTo: function( speed, to, easing, callback ) { + + // show any hidden elements after setting opacity to 0 + return this.filter( isHidden ).css( "opacity", 0 ).show() + + // animate to the value specified + .end().animate({ opacity: to }, speed, easing, callback ); + }, + animate: function( prop, speed, easing, callback ) { + var empty = jQuery.isEmptyObject( prop ), + optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + // Operate on a copy of prop so per-property easing won't be lost + var anim = Animation( this, jQuery.extend( {}, prop ), optall ); + + // Empty animations resolve immediately + if ( empty ) { + anim.stop( true ); + } + }; + + return empty || optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( hooks ) { + var stop = hooks.stop; + delete hooks.stop; + stop( gotoEnd ); + }; + + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue && type !== false ) { + this.queue( type || "fx", [] ); + } + + return this.each(function() { + var dequeue = true, + index = type != null && type + "queueHooks", + timers = jQuery.timers, + data = jQuery._data( this ); + + if ( index ) { + if ( data[ index ] && data[ index ].stop ) { + stopQueue( data[ index ] ); + } + } else { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { + stopQueue( data[ index ] ); + } + } + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { + timers[ index ].anim.stop( gotoEnd ); + dequeue = false; + timers.splice( index, 1 ); + } + } + + // start the next in the queue if the last step wasn't forced + // timers currently will call their complete callbacks, which will dequeue + // but only if they were gotoEnd + if ( dequeue || !gotoEnd ) { + jQuery.dequeue( this, type ); + } + }); + } +}); + +// Generate parameters to create a standard animation +function genFx( type, includeWidth ) { + var which, + attrs = { height: type }, + i = 0; + + // if we include width, step value is 1 to do all cssExpand values, + // if we don't include width, step value is 2 to skip over Left and Right + includeWidth = includeWidth? 1 : 0; + for( ; i < 4 ; i += 2 - includeWidth ) { + which = cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } + + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } + + return attrs; +} + +// Generate shortcuts for custom animations +jQuery.each({ + slideDown: genFx("show"), + slideUp: genFx("hide"), + slideToggle: genFx("toggle"), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +}); + +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + jQuery.isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing + }; + + opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : + opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; + + // normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + // Queueing + opt.old = opt.complete; + + opt.complete = function() { + if ( jQuery.isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } + }; + + return opt; +}; + +jQuery.easing = { + linear: function( p ) { + return p; + }, + swing: function( p ) { + return 0.5 - Math.cos( p*Math.PI ) / 2; + } +}; + +jQuery.timers = []; +jQuery.fx = Tween.prototype.init; +jQuery.fx.tick = function() { + var timer, + timers = jQuery.timers, + i = 0; + + fxNow = jQuery.now(); + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + // Checks the timer has not already been removed + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + fxNow = undefined; +}; + +jQuery.fx.timer = function( timer ) { + if ( timer() && jQuery.timers.push( timer ) && !timerId ) { + timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval ); + } +}; + +jQuery.fx.interval = 13; + +jQuery.fx.stop = function() { + clearInterval( timerId ); + timerId = null; +}; + +jQuery.fx.speeds = { + slow: 600, + fast: 200, + // Default speed + _default: 400 +}; + +// Back Compat <1.8 extension point +jQuery.fx.step = {}; + +if ( jQuery.expr && jQuery.expr.filters ) { + jQuery.expr.filters.animated = function( elem ) { + return jQuery.grep(jQuery.timers, function( fn ) { + return elem === fn.elem; + }).length; + }; +} +var rroot = /^(?:body|html)$/i; + +jQuery.fn.offset = function( options ) { + if ( arguments.length ) { + return options === undefined ? + this : + this.each(function( i ) { + jQuery.offset.setOffset( this, options, i ); + }); + } + + var docElem, body, win, clientTop, clientLeft, scrollTop, scrollLeft, + box = { top: 0, left: 0 }, + elem = this[ 0 ], + doc = elem && elem.ownerDocument; + + if ( !doc ) { + return; + } + + if ( (body = doc.body) === elem ) { + return jQuery.offset.bodyOffset( elem ); + } + + docElem = doc.documentElement; + + // Make sure it's not a disconnected DOM node + if ( !jQuery.contains( docElem, elem ) ) { + return box; + } + + // If we don't have gBCR, just use 0,0 rather than error + // BlackBerry 5, iOS 3 (original iPhone) + if ( typeof elem.getBoundingClientRect !== "undefined" ) { + box = elem.getBoundingClientRect(); + } + win = getWindow( doc ); + clientTop = docElem.clientTop || body.clientTop || 0; + clientLeft = docElem.clientLeft || body.clientLeft || 0; + scrollTop = win.pageYOffset || docElem.scrollTop; + scrollLeft = win.pageXOffset || docElem.scrollLeft; + return { + top: box.top + scrollTop - clientTop, + left: box.left + scrollLeft - clientLeft + }; +}; + +jQuery.offset = { + + bodyOffset: function( body ) { + var top = body.offsetTop, + left = body.offsetLeft; + + if ( jQuery.support.doesNotIncludeMarginInBodyOffset ) { + top += parseFloat( jQuery.css(body, "marginTop") ) || 0; + left += parseFloat( jQuery.css(body, "marginLeft") ) || 0; + } + + return { top: top, left: left }; + }, + + setOffset: function( elem, options, i ) { + var position = jQuery.css( elem, "position" ); + + // set position first, in-case top/left are set even on static elem + if ( position === "static" ) { + elem.style.position = "relative"; + } + + var curElem = jQuery( elem ), + curOffset = curElem.offset(), + curCSSTop = jQuery.css( elem, "top" ), + curCSSLeft = jQuery.css( elem, "left" ), + calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1, + props = {}, curPosition = {}, curTop, curLeft; + + // need to be able to calculate position if either top or left is auto and position is either absolute or fixed + if ( calculatePosition ) { + curPosition = curElem.position(); + curTop = curPosition.top; + curLeft = curPosition.left; + } else { + curTop = parseFloat( curCSSTop ) || 0; + curLeft = parseFloat( curCSSLeft ) || 0; + } + + if ( jQuery.isFunction( options ) ) { + options = options.call( elem, i, curOffset ); + } + + if ( options.top != null ) { + props.top = ( options.top - curOffset.top ) + curTop; + } + if ( options.left != null ) { + props.left = ( options.left - curOffset.left ) + curLeft; + } + + if ( "using" in options ) { + options.using.call( elem, props ); + } else { + curElem.css( props ); + } + } +}; + + +jQuery.fn.extend({ + + position: function() { + if ( !this[0] ) { + return; + } + + var elem = this[0], + + // Get *real* offsetParent + offsetParent = this.offsetParent(), + + // Get correct offsets + offset = this.offset(), + parentOffset = rroot.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset(); + + // Subtract element margins + // note: when an element has margin: auto the offsetLeft and marginLeft + // are the same in Safari causing offset.left to incorrectly be 0 + offset.top -= parseFloat( jQuery.css(elem, "marginTop") ) || 0; + offset.left -= parseFloat( jQuery.css(elem, "marginLeft") ) || 0; + + // Add offsetParent borders + parentOffset.top += parseFloat( jQuery.css(offsetParent[0], "borderTopWidth") ) || 0; + parentOffset.left += parseFloat( jQuery.css(offsetParent[0], "borderLeftWidth") ) || 0; + + // Subtract the two offsets + return { + top: offset.top - parentOffset.top, + left: offset.left - parentOffset.left + }; + }, + + offsetParent: function() { + return this.map(function() { + var offsetParent = this.offsetParent || document.body; + while ( offsetParent && (!rroot.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent || document.body; + }); + } +}); + + +// Create scrollLeft and scrollTop methods +jQuery.each( {scrollLeft: "pageXOffset", scrollTop: "pageYOffset"}, function( method, prop ) { + var top = /Y/.test( prop ); + + jQuery.fn[ method ] = function( val ) { + return jQuery.access( this, function( elem, method, val ) { + var win = getWindow( elem ); + + if ( val === undefined ) { + return win ? (prop in win) ? win[ prop ] : + win.document.documentElement[ method ] : + elem[ method ]; + } + + if ( win ) { + win.scrollTo( + !top ? val : jQuery( win ).scrollLeft(), + top ? val : jQuery( win ).scrollTop() + ); + + } else { + elem[ method ] = val; + } + }, method, val, arguments.length, null ); + }; +}); + +function getWindow( elem ) { + return jQuery.isWindow( elem ) ? + elem : + elem.nodeType === 9 ? + elem.defaultView || elem.parentWindow : + false; +} +// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods +jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { + jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) { + // margin is only for outerHeight, outerWidth + jQuery.fn[ funcName ] = function( margin, value ) { + var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ), + extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" ); + + return jQuery.access( this, function( elem, type, value ) { + var doc; + + if ( jQuery.isWindow( elem ) ) { + // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there + // isn't a whole lot we can do. See pull request at this URL for discussion: + // https://github.com/jquery/jquery/pull/764 + return elem.document.documentElement[ "client" + name ]; + } + + // Get document width or height + if ( elem.nodeType === 9 ) { + doc = elem.documentElement; + + // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest + // unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it. + return Math.max( + elem.body[ "scroll" + name ], doc[ "scroll" + name ], + elem.body[ "offset" + name ], doc[ "offset" + name ], + doc[ "client" + name ] + ); + } + + return value === undefined ? + // Get width or height on the element, requesting but not forcing parseFloat + jQuery.css( elem, type, value, extra ) : + + // Set width or height on the element + jQuery.style( elem, type, value, extra ); + }, type, chainable ? margin : undefined, chainable, null ); + }; + }); +}); +// Expose jQuery to the global object +window.jQuery = window.$ = jQuery; + +// Expose jQuery as an AMD module, but only for AMD loaders that +// understand the issues with loading multiple versions of jQuery +// in a page that all might call define(). The loader will indicate +// they have special allowances for multiple jQuery versions by +// specifying define.amd.jQuery = true. Register as a named module, +// since jQuery can be concatenated with other files that may use define, +// but not use a proper concatenation script that understands anonymous +// AMD modules. A named AMD is safest and most robust way to register. +// Lowercase jquery is used because AMD module names are derived from +// file names, and jQuery is normally delivered in a lowercase file name. +// Do this after creating the global so that if an AMD module wants to call +// noConflict to hide this version of jQuery, it will work. +if ( typeof define === "function" && define.amd && define.amd.jQuery ) { + define( "jquery", [], function () { return jQuery; } ); +} + +})( window );