From d1d495fdda025859ff0e340c1b59b4145df29952 Mon Sep 17 00:00:00 2001 From: Tyron Madlener Date: Tue, 31 May 2011 12:03:17 +0200 Subject: [PATCH] Highcarts (unminivied) source files --- js/highcharts/exporting.src.js | 673 ++ js/highcharts/highcharts.src.js | 10671 ++++++++++++++++++++++++++++++ 2 files changed, 11344 insertions(+) create mode 100644 js/highcharts/exporting.src.js create mode 100644 js/highcharts/highcharts.src.js diff --git a/js/highcharts/exporting.src.js b/js/highcharts/exporting.src.js new file mode 100644 index 0000000000..4577f67a9a --- /dev/null +++ b/js/highcharts/exporting.src.js @@ -0,0 +1,673 @@ +/** + * @license Highcharts JS v2.1.4 (2011-03-02) + * Exporting module + * + * (c) 2010 Torstein Hønsi + * + * License: www.highcharts.com/license + */ + +// JSLint options: +/*global Highcharts, document, window, Math, setTimeout */ + +(function() { // encapsulate + +// create shortcuts +var HC = Highcharts, + Chart = HC.Chart, + addEvent = HC.addEvent, + createElement = HC.createElement, + discardElement = HC.discardElement, + css = HC.css, + merge = HC.merge, + each = HC.each, + extend = HC.extend, + math = Math, + mathMax = math.max, + doc = document, + win = window, + hasTouch = 'ontouchstart' in doc.documentElement, + M = 'M', + L = 'L', + DIV = 'div', + HIDDEN = 'hidden', + NONE = 'none', + PREFIX = 'highcharts-', + ABSOLUTE = 'absolute', + PX = 'px', + + + + // Add language and get the defaultOptions + defaultOptions = HC.setOptions({ + lang: { + downloadPNG: 'Download PNG image', + downloadJPEG: 'Download JPEG image', + downloadPDF: 'Download PDF document', + downloadSVG: 'Download SVG vector image', + exportButtonTitle: 'Export to raster or vector image', + printButtonTitle: 'Print the chart' + } + }); + +// Buttons and menus are collected in a separate config option set called 'navigation'. +// This can be extended later to add control buttons like zoom and pan right click menus. +defaultOptions.navigation = { + menuStyle: { + border: '1px solid #A0A0A0', + background: '#FFFFFF' + }, + menuItemStyle: { + padding: '0 5px', + background: NONE, + color: '#303030', + fontSize: hasTouch ? '14px' : '11px' + }, + menuItemHoverStyle: { + background: '#4572A5', + color: '#FFFFFF' + }, + + buttonOptions: { + align: 'right', + backgroundColor: { + linearGradient: [0, 0, 0, 20], + stops: [ + [0.4, '#F7F7F7'], + [0.6, '#E3E3E3'] + ] + }, + borderColor: '#B0B0B0', + borderRadius: 3, + borderWidth: 1, + //enabled: true, + height: 20, + hoverBorderColor: '#909090', + hoverSymbolFill: '#81A7CF', + hoverSymbolStroke: '#4572A5', + symbolFill: '#E0E0E0', + //symbolSize: 12, + symbolStroke: '#A0A0A0', + //symbolStrokeWidth: 1, + symbolX: 11.5, + symbolY: 10.5, + verticalAlign: 'top', + width: 24, + y: 10 + } +}; + + + +// Add the export related options +defaultOptions.exporting = { + //enabled: true, + //filename: 'chart', + type: 'image/png', + url: 'http://export.highcharts.com/', + width: 800, + buttons: { + exportButton: { + //enabled: true, + symbol: 'exportIcon', + x: -10, + symbolFill: '#A8BF77', + hoverSymbolFill: '#768F3E', + _titleKey: 'exportButtonTitle', + menuItems: [{ + textKey: 'downloadPNG', + onclick: function() { + this.exportChart(); + } + }, { + textKey: 'downloadJPEG', + onclick: function() { + this.exportChart({ + type: 'image/jpeg' + }); + } + }, { + textKey: 'downloadPDF', + onclick: function() { + this.exportChart({ + type: 'application/pdf' + }); + } + }, { + textKey: 'downloadSVG', + onclick: function() { + this.exportChart({ + type: 'image/svg+xml' + }); + } + }/*, { + text: 'View SVG', + onclick: function() { + var svg = this.getSVG() + .replace(//g, '>'); + + doc.body.innerHTML = '
'+ svg +'
'; + } + }*/] + + }, + printButton: { + //enabled: true, + symbol: 'printIcon', + x: -36, + symbolFill: '#B5C9DF', + hoverSymbolFill: '#779ABF', + _titleKey: 'printButtonTitle', + onclick: function() { + this.print(); + } + } + } +}; + + + +extend(Chart.prototype, { + /** + * Return an SVG representation of the chart + * + * @param additionalOptions {Object} Additional chart options for the generated SVG representation + */ + getSVG: function(additionalOptions) { + var chart = this, + chartCopy, + sandbox, + svg, + seriesOptions, + config, + pointOptions, + pointMarker, + options = merge(chart.options, additionalOptions); // copy the options and add extra options + + // IE compatibility hack for generating SVG content that it doesn't really understand + if (!doc.createElementNS) { + doc.createElementNS = function(ns, tagName) { + var elem = doc.createElement(tagName); + elem.getBBox = function() { + return chart.renderer.Element.prototype.getBBox.apply({ element: elem }); + }; + return elem; + }; + } + + // create a sandbox where a new chart will be generated + sandbox = createElement(DIV, null, { + position: ABSOLUTE, + top: '-9999em', + width: chart.chartWidth + PX, + height: chart.chartHeight + PX + }, doc.body); + + // override some options + extend(options.chart, { + renderTo: sandbox, + forExport: true + }); + options.exporting.enabled = false; // hide buttons in print + options.chart.plotBackgroundImage = null; // the converter doesn't handle images + // prepare for replicating the chart + options.series = []; + each(chart.series, function(serie) { + seriesOptions = serie.options; + + seriesOptions.animation = false; // turn off animation + seriesOptions.showCheckbox = false; + + // remove image markers + if (seriesOptions && seriesOptions.marker && /^url\(/.test(seriesOptions.marker.symbol)) { + seriesOptions.marker.symbol = 'circle'; + } + + seriesOptions.data = []; + + each(serie.data, function(point) { + + // extend the options by those values that can be expressed in a number or array config + config = point.config; + pointOptions = { + x: point.x, + y: point.y, + name: point.name + }; + + if (typeof config == 'object' && point.config && config.constructor != Array) { + extend(pointOptions, config); + } + + seriesOptions.data.push(pointOptions); // copy fresh updated data + + // remove image markers + pointMarker = point.config && point.config.marker; + if (pointMarker && /^url\(/.test(pointMarker.symbol)) { + delete pointMarker.symbol; + } + }); + + options.series.push(seriesOptions); + }); + + // generate the chart copy + chartCopy = new Highcharts.Chart(options); + + // get the SVG from the container's innerHTML + svg = chartCopy.container.innerHTML; + + // free up memory + options = null; + chartCopy.destroy(); + discardElement(sandbox); + + // sanitize + svg = svg + .replace(/zIndex="[^"]+"/g, '') + .replace(/isShadow="[^"]+"/g, '') + .replace(/symbolName="[^"]+"/g, '') + .replace(/jQuery[0-9]+="[^"]+"/g, '') + .replace(/isTracker="[^"]+"/g, '') + .replace(/url\([^#]+#/g, 'url(#') + /*.replace(/')*/ + /* This fails in IE < 8 + .replace(/([0-9]+)\.([0-9]+)/g, function(s1, s2, s3) { // round off to save weight + return s2 +'.'+ s3[0]; + })*/ + + // IE specific + .replace(/id=([^" >]+)/g, 'id="$1"') + .replace(/class=([^" ]+)/g, 'class="$1"') + .replace(/ transform /g, ' ') + .replace(/:(path|rect)/g, '$1') + .replace(/style="([^"]+)"/g, function(s) { + return s.toLowerCase(); + }); + + // IE9 beta bugs with innerHTML. Test again with final IE9. + svg = svg.replace(/(url\(#highcharts-[0-9]+)"/g, '$1') + .replace(/"/g, "'"); + if (svg.match(/ xmlns="/g).length == 2) { + svg = svg.replace(/xmlns="[^"]+"/, ''); + } + + return svg; + }, + + /** + * Submit the SVG representation of the chart to the server + * @param {Object} options Exporting options. Possible members are url, type and width. + * @param {Object} chartOptions Additional chart options for the SVG representation of the chart + */ + exportChart: function(options, chartOptions) { + var form, + chart = this, + svg = chart.getSVG(chartOptions); + + // merge the options + options = merge(chart.options.exporting, options); + + // create the form + form = createElement('form', { + method: 'post', + action: options.url + }, { + display: NONE + }, doc.body); + + // add the values + each(['filename', 'type', 'width', 'svg'], function(name) { + createElement('input', { + type: HIDDEN, + name: name, + value: { + filename: options.filename || 'chart', + type: options.type, + width: options.width, + svg: svg + }[name] + }, null, form); + }); + + // submit + form.submit(); + + // clean up + discardElement(form); + }, + + /** + * Print the chart + */ + print: function() { + + var chart = this, + container = chart.container, + origDisplay = [], + origParent = container.parentNode, + body = doc.body, + childNodes = body.childNodes; + + if (chart.isPrinting) { // block the button while in printing mode + return; + } + + chart.isPrinting = true; + + // hide all body content + each(childNodes, function(node, i) { + if (node.nodeType == 1) { + origDisplay[i] = node.style.display; + node.style.display = NONE; + } + }); + + // pull out the chart + body.appendChild(container); + + // print + win.print(); + + // allow the browser to prepare before reverting + setTimeout(function() { + + // put the chart back in + origParent.appendChild(container); + + // restore all body content + each(childNodes, function(node, i) { + if (node.nodeType == 1) { + node.style.display = origDisplay[i]; + } + }); + + chart.isPrinting = false; + + }, 1000); + + }, + + /** + * Display a popup menu for choosing the export type + * + * @param {String} name An identifier for the menu + * @param {Array} items A collection with text and onclicks for the items + * @param {Number} x The x position of the opener button + * @param {Number} y The y position of the opener button + * @param {Number} width The width of the opener button + * @param {Number} height The height of the opener button + */ + contextMenu: function(name, items, x, y, width, height) { + var chart = this, + navOptions = chart.options.navigation, + menuItemStyle = navOptions.menuItemStyle, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + cacheName = 'cache-'+ name, + menu = chart[cacheName], + menuPadding = mathMax(width, height), // for mouse leave detection + boxShadow = '3px 3px 10px #888', + innerMenu, + hide, + menuStyle; + + // create the menu only the first time + if (!menu) { + + // create a HTML element above the SVG + chart[cacheName] = menu = createElement(DIV, { + className: PREFIX + name + }, { + position: ABSOLUTE, + zIndex: 1000, + padding: menuPadding + PX + }, chart.container); + + innerMenu = createElement(DIV, null, + extend({ + MozBoxShadow: boxShadow, + WebkitBoxShadow: boxShadow, + boxShadow: boxShadow + }, navOptions.menuStyle) , menu); + + // hide on mouse out + hide = function() { + css(menu, { display: NONE }); + }; + + addEvent(menu, 'mouseleave', hide); + + + // create the items + each(items, function(item) { + if (item) { + var div = createElement(DIV, { + onmouseover: function() { + css(this, navOptions.menuItemHoverStyle); + }, + onmouseout: function() { + css(this, menuItemStyle); + }, + innerHTML: item.text || HC.getOptions().lang[item.textKey] + }, extend({ + cursor: 'pointer' + }, menuItemStyle), innerMenu); + + div[hasTouch ? 'ontouchstart' : 'onclick'] = function() { + hide(); + item.onclick.apply(chart, arguments); + }; + + } + }); + + chart.exportMenuWidth = menu.offsetWidth; + chart.exportMenuHeight = menu.offsetHeight; + } + + menuStyle = { display: 'block' }; + + // if outside right, right align it + if (x + chart.exportMenuWidth > chartWidth) { + menuStyle.right = (chartWidth - x - width - menuPadding) + PX; + } else { + menuStyle.left = (x - menuPadding) + PX; + } + // if outside bottom, bottom align it + if (y + height + chart.exportMenuHeight > chartHeight) { + menuStyle.bottom = (chartHeight - y - menuPadding) + PX; + } else { + menuStyle.top = (y + height - menuPadding) + PX; + } + + css(menu, menuStyle); + }, + + /** + * Add the export button to the chart + */ + addButton: function(options) { + var chart = this, + renderer = chart.renderer, + btnOptions = merge(chart.options.navigation.buttonOptions, options), + onclick = btnOptions.onclick, + menuItems = btnOptions.menuItems, + /*position = chart.getAlignment(btnOptions), + buttonLeft = position.x, + buttonTop = position.y,*/ + buttonWidth = btnOptions.width, + buttonHeight = btnOptions.height, + box, + symbol, + button, + borderWidth = btnOptions.borderWidth, + boxAttr = { + stroke: btnOptions.borderColor + + }, + symbolAttr = { + stroke: btnOptions.symbolStroke, + fill: btnOptions.symbolFill + }; + + if (btnOptions.enabled === false) { + return; + } + + // element to capture the click + function revert() { + symbol.attr(symbolAttr); + box.attr(boxAttr); + } + + // the box border + box = renderer.rect( + 0, + 0, + buttonWidth, + buttonHeight, + btnOptions.borderRadius, + borderWidth + ) + //.translate(buttonLeft, buttonTop) // to allow gradients + .align(btnOptions, true) + .attr(extend({ + fill: btnOptions.backgroundColor, + 'stroke-width': borderWidth, + zIndex: 19 + }, boxAttr)).add(); + + // the invisible element to track the clicks + button = renderer.rect( + 0, + 0, + buttonWidth, + buttonHeight, + 0 + ) + .align(btnOptions) + .attr({ + fill: 'rgba(255, 255, 255, 0.001)', + title: HC.getOptions().lang[btnOptions._titleKey], + zIndex: 21 + }).css({ + cursor: 'pointer' + }) + .on('mouseover', function() { + symbol.attr({ + stroke: btnOptions.hoverSymbolStroke, + fill: btnOptions.hoverSymbolFill + }); + box.attr({ + stroke: btnOptions.hoverBorderColor + }); + }) + .on('mouseout', revert) + .on('click', revert) + .add(); + + //addEvent(button.element, 'click', revert); + + // add the click event + if (menuItems) { + onclick = function(e) { + revert(); + var bBox = button.getBBox(); + chart.contextMenu('export-menu', menuItems, bBox.x, bBox.y, buttonWidth, buttonHeight); + }; + } + /*addEvent(button.element, 'click', function() { + onclick.apply(chart, arguments); + });*/ + button.on('click', function() { + onclick.apply(chart, arguments); + }); + + // the icon + symbol = renderer.symbol( + btnOptions.symbol, + btnOptions.symbolX, + btnOptions.symbolY, + (btnOptions.symbolSize || 12) / 2 + ) + .align(btnOptions, true) + .attr(extend(symbolAttr, { + 'stroke-width': btnOptions.symbolStrokeWidth || 1, + zIndex: 20 + })).add(); + + + + } +}); + +// Create the export icon +HC.Renderer.prototype.symbols.exportIcon = function(x, y, radius) { + return [ + M, // the disk + x - radius, y + radius, + L, + x + radius, y + radius, + x + radius, y + radius * 0.5, + x - radius, y + radius * 0.5, + 'Z', + M, // the arrow + x, y + radius * 0.5, + L, + x - radius * 0.5, y - radius / 3, + x - radius / 6, y - radius / 3, + x - radius / 6, y - radius, + x + radius / 6, y - radius, + x + radius / 6, y - radius / 3, + x + radius * 0.5, y - radius / 3, + 'Z' + ]; +}; +// Create the print icon +HC.Renderer.prototype.symbols.printIcon = function(x, y, radius) { + return [ + M, // the printer + x - radius, y + radius * 0.5, + L, + x + radius, y + radius * 0.5, + x + radius, y - radius / 3, + x - radius, y - radius / 3, + 'Z', + M, // the upper sheet + x - radius * 0.5, y - radius / 3, + L, + x - radius * 0.5, y - radius, + x + radius * 0.5, y - radius, + x + radius * 0.5, y - radius / 3, + 'Z', + M, // the lower sheet + x - radius * 0.5, y + radius * 0.5, + L, + x - radius * 0.75, y + radius, + x + radius * 0.75, y + radius, + x + radius * 0.5, y + radius * 0.5, + 'Z' + ]; +}; + + +// Add the buttons on chart load +Chart.prototype.callbacks.push(function(chart) { + var n, + exportingOptions = chart.options.exporting, + buttons = exportingOptions.buttons; + + if (exportingOptions.enabled !== false) { + + for (n in buttons) { + chart.addButton(buttons[n]); + } + } +}); + + +})(); \ No newline at end of file diff --git a/js/highcharts/highcharts.src.js b/js/highcharts/highcharts.src.js new file mode 100644 index 0000000000..c15862b4f0 --- /dev/null +++ b/js/highcharts/highcharts.src.js @@ -0,0 +1,10671 @@ +// ==ClosureCompiler== +// @compilation_level SIMPLE_OPTIMIZATIONS + +/** + * @license Highcharts JS v2.1.4 (2011-03-02) + * + * (c) 2009-2010 Torstein Hønsi + * + * License: www.highcharts.com/license + */ + +// JSLint options: +/*jslint forin: true */ +/*global document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $ */ + +(function() { +// encapsulated variables +var doc = document, + win = window, + math = Math, + mathRound = math.round, + mathFloor = math.floor, + mathCeil = math.ceil, + mathMax = math.max, + mathMin = math.min, + mathAbs = math.abs, + mathCos = math.cos, + mathSin = math.sin, + mathPI = math.PI, + deg2rad = mathPI * 2 / 360, + + + // some variables + userAgent = navigator.userAgent, + isIE = /msie/i.test(userAgent) && !win.opera, + docMode8 = doc.documentMode == 8, + isWebKit = /AppleWebKit/.test(userAgent), + isFirefox = /Firefox/.test(userAgent), + //hasSVG = win.SVGAngle || doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1"), + hasSVG = !!doc.createElementNS && !!doc.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGRect, + SVG_NS = 'http://www.w3.org/2000/svg', + hasTouch = 'ontouchstart' in doc.documentElement, + colorCounter, + symbolCounter, + symbolSizes = {}, + idCounter = 0, + timeFactor = 1, // 1 = JavaScript time, 1000 = Unix time + garbageBin, + defaultOptions, + dateFormat, // function + globalAnimation, + pathAnim, + + + // some constants for frequently used strings + UNDEFINED, + DIV = 'div', + ABSOLUTE = 'absolute', + RELATIVE = 'relative', + HIDDEN = 'hidden', + PREFIX = 'highcharts-', + VISIBLE = 'visible', + PX = 'px', + NONE = 'none', + M = 'M', + L = 'L', + /* + * Empirical lowest possible opacities for TRACKER_FILL + * IE6: 0.002 + * IE7: 0.002 + * IE8: 0.002 + * IE9: 0.00000000001 (unlimited) + * FF: 0.00000000001 (unlimited) + * Chrome: 0.000001 + * Safari: 0.000001 + * Opera: 0.00000000001 (unlimited) + */ + TRACKER_FILL = 'rgba(192,192,192,'+ (hasSVG ? 0.000001 : 0.002) +')', // invisible but clickable + NORMAL_STATE = '', + HOVER_STATE = 'hover', + SELECT_STATE = 'select', + + // time methods, changed based on whether or not UTC is used + makeTime, + getMinutes, + getHours, + getDay, + getDate, + getMonth, + getFullYear, + setMinutes, + setHours, + setDate, + setMonth, + setFullYear, + + // check for a custom HighchartsAdapter defined prior to this file + globalAdapter = win.HighchartsAdapter, + adapter = globalAdapter || {}, + + // Utility functions. If the HighchartsAdapter is not defined, adapter is an empty object + // and all the utility functions will be null. In that case they are populated by the + // default adapters below. + each = adapter.each, + grep = adapter.grep, + map = adapter.map, + merge = adapter.merge, + hyphenate = adapter.hyphenate, + addEvent = adapter.addEvent, + removeEvent = adapter.removeEvent, + fireEvent = adapter.fireEvent, + animate = adapter.animate, + stop = adapter.stop, + + // lookup over the types and the associated classes + seriesTypes = {}, + hoverChart; + +/** + * Extend an object with the members of another + * @param {Object} a The object to be extended + * @param {Object} b The object to add to the first one + */ +function extend(a, b) { + if (!a) { + a = {}; + } + for (var n in b) { + a[n] = b[n]; + } + return a; +} + +/** + * Shortcut for parseInt + * @param {Object} s + */ +function pInt(s, mag) { + return parseInt(s, mag || 10); +} + +/** + * Check for string + * @param {Object} s + */ +function isString(s) { + return typeof s == 'string'; +} + +/** + * Check for object + * @param {Object} obj + */ +function isObject(obj) { + return typeof obj == 'object'; +} + +/** + * Check for number + * @param {Object} n + */ +function isNumber(n) { + return typeof n == 'number'; +} + +/** + * Remove last occurence of an item from an array + * @param {Array} arr + * @param {Mixed} item + */ +function erase(arr, item) { + var i = arr.length; + while (i--) { + if (arr[i] == item) { + arr.splice(i, 1); + break; + } + } + //return arr; +} + +/** + * Returns true if the object is not null or undefined. Like MooTools' $.defined. + * @param {Object} obj + */ +function defined (obj) { + return obj !== UNDEFINED && obj !== null; +} + +/** + * Set or get an attribute or an object of attributes. Can't use jQuery attr because + * it attempts to set expando properties on the SVG element, which is not allowed. + * + * @param {Object} elem The DOM element to receive the attribute(s) + * @param {String|Object} prop The property or an abject of key-value pairs + * @param {String} value The value if a single property is set + */ +function attr(elem, prop, value) { + var key, + setAttribute = 'setAttribute', + ret; + + // if the prop is a string + if (isString(prop)) { + // set the value + if (defined(value)) { + + elem[setAttribute](prop, value); + + // get the value + } else if (elem && elem.getAttribute) { // elem not defined when printing pie demo... + ret = elem.getAttribute(prop); + } + + // else if prop is defined, it is a hash of key/value pairs + } else if (defined(prop) && isObject(prop)) { + for (key in prop) { + elem[setAttribute](key, prop[key]); + } + } + return ret; +} +/** + * Check if an element is an array, and if not, make it into an array. Like + * MooTools' $.splat. + */ +function splat(obj) { + if (!obj || obj.constructor != Array) { + obj = [obj]; + } + return obj; +} + + + +/** + * Return the first value that is defined. Like MooTools' $.pick. + */ +function pick() { + var args = arguments, + i, + arg, + length = args.length; + for (i = 0; i < length; i++) { + arg = args[i]; + if (typeof arg !== 'undefined' && arg !== null) { + return arg; + } + } +} +/** + * Make a style string from a JS object + * @param {Object} style + */ +function serializeCSS(style) { + var s = '', + key; + // serialize the declaration + for (key in style) { + s += hyphenate(key) +':'+ style[key] + ';'; + } + return s; + +} +/** + * Set CSS on a give element + * @param {Object} el + * @param {Object} styles + */ +function css (el, styles) { + if (isIE) { + if (styles && styles.opacity !== UNDEFINED) { + styles.filter = 'alpha(opacity='+ (styles.opacity * 100) +')'; + } + } + extend(el.style, styles); +} + +/** + * Utility function to create element with attributes and styles + * @param {Object} tag + * @param {Object} attribs + * @param {Object} styles + * @param {Object} parent + * @param {Object} nopad + */ +function createElement (tag, attribs, styles, parent, nopad) { + var el = doc.createElement(tag); + if (attribs) { + extend(el, attribs); + } + if (nopad) { + css(el, {padding: 0, border: NONE, margin: 0}); + } + if (styles) { + css(el, styles); + } + if (parent) { + parent.appendChild(el); + } + return el; +} + +/** + * Set the global animation to either a given value, or fall back to the + * given chart's animation option + * @param {Object} animation + * @param {Object} chart + */ +function setAnimation(animation, chart) { + globalAnimation = pick(animation, chart.animation); +} + +/* + * Define the adapter for frameworks. If an external adapter is not defined, + * Highcharts reverts to the built-in jQuery adapter. + */ +if (globalAdapter && globalAdapter.init) { + globalAdapter.init(); +} +if (!globalAdapter && win.jQuery) { + var jQ = jQuery; + + /** + * Utility for iterating over an array. Parameters are reversed compared to jQuery. + * @param {Array} arr + * @param {Function} fn + */ + each = function(arr, fn) { + for (var i = 0, len = arr.length; i < len; i++) { + if (fn.call(arr[i], arr[i], i, arr) === false) { + return i; + } + } + }; + + /** + * Filter an array + */ + grep = jQ.grep; + + /** + * Map an array + * @param {Array} arr + * @param {Function} fn + */ + map = function(arr, fn){ + //return jQuery.map(arr, fn); + var results = []; + for (var i = 0, len = arr.length; i < len; i++) { + results[i] = fn.call(arr[i], arr[i], i, arr); + } + return results; + + }; + + /** + * Deep merge two objects and return a third object + */ + merge = function(){ + var args = arguments; + return jQ.extend(true, null, args[0], args[1], args[2], args[3]); + }; + + /** + * Convert a camelCase string to a hyphenated string + * @param {String} str + */ + hyphenate = function (str) { + return str.replace(/([A-Z])/g, function(a, b){ return '-'+ b.toLowerCase(); }); + }; + + /** + * Add an event listener + * @param {Object} el A HTML element or custom object + * @param {String} event The event type + * @param {Function} fn The event handler + */ + addEvent = function (el, event, fn){ + jQ(el).bind(event, fn); + }; + + /** + * Remove event added with addEvent + * @param {Object} el The object + * @param {String} eventType The event type. Leave blank to remove all events. + * @param {Function} handler The function to remove + */ + removeEvent = function(el, eventType, handler) { + // workaround for jQuery issue with unbinding custom events: + // http://forum.jquery.com/topic/javascript-error-when-unbinding-a-custom-event-using-jquery-1-4-2 + var func = doc.removeEventListener ? 'removeEventListener' : 'detachEvent'; + if (doc[func] && !el[func]) { + el[func] = function() {}; + } + + jQ(el).unbind(eventType, handler); + }; + + /** + * Fire an event on a custom object + * @param {Object} el + * @param {String} type + * @param {Object} eventArguments + * @param {Function} defaultFunction + */ + fireEvent = function(el, type, eventArguments, defaultFunction) { + var event = jQ.Event(type), + detachedType = 'detached'+ type; + extend(event, eventArguments); + + // Prevent jQuery from triggering the object method that is named the + // same as the event. For example, if the event is 'select', jQuery + // attempts calling el.select and it goes into a loop. + if (el[type]) { + el[detachedType] = el[type]; + el[type] = null; + } + + // trigger it + jQ(el).trigger(event); + + // attach the method + if (el[detachedType]) { + el[type] = el[detachedType]; + el[detachedType] = null; + } + + if (defaultFunction && !event.isDefaultPrevented()) { + defaultFunction(event); + } + }; + + /** + * Animate a HTML element or SVG element wrapper + * @param {Object} el + * @param {Object} params + * @param {Object} options jQuery-like animation options: duration, easing, callback + */ + animate = function (el, params, options) { + var $el = jQ(el); + if (params.d) { + el.toD = params.d; // keep the array form for paths, used in jQ.fx.step.d + params.d = 1; // because in jQuery, animating to an array has a different meaning + } + + $el.stop(); + $el.animate(params, options); + + }; + /** + * Stop running animation + */ + stop = function (el) { + jQ(el).stop(); + }; + + + // extend jQuery + jQ.extend( jQ.easing, { + easeOutQuad: function (x, t, b, c, d) { + return -c *(t/=d)*(t-2) + b; + } + }); + + // extend the animate function to allow SVG animations + var oldStepDefault = jQuery.fx.step._default, + oldCur = jQuery.fx.prototype.cur; + + // do the step + jQ.fx.step._default = function(fx){ + var elem = fx.elem; + if (elem.attr) { // is SVG element wrapper + elem.attr(fx.prop, fx.now); + } else { + oldStepDefault.apply(this, arguments); + } + }; + // animate paths + jQ.fx.step.d = function(fx) { + var elem = fx.elem; + + + // Normally start and end should be set in state == 0, but sometimes, + // for reasons unknown, this doesn't happen. Perhaps state == 0 is skipped + // in these cases + if (!fx.started) { + var ends = pathAnim.init(elem, elem.d, elem.toD); + fx.start = ends[0]; + fx.end = ends[1]; + fx.started = true; + } + + + // interpolate each value of the path + elem.attr('d', pathAnim.step(fx.start, fx.end, fx.pos, elem.toD)); + + }; + // get the current value + jQ.fx.prototype.cur = function() { + var elem = this.elem, + r; + if (elem.attr) { // is SVG element wrapper + r = elem.attr(this.prop); + } else { + r = oldCur.apply(this, arguments); + } + return r; + }; +} + + +/** + * Add a global listener for mousemove events + */ +/*addEvent(doc, 'mousemove', function(e) { + if (globalMouseMove) { + globalMouseMove(e); + } +});*/ + +/** + * Path interpolation algorithm used across adapters + */ +pathAnim = { + /** + * Prepare start and end values so that the path can be animated one to one + */ + init: function(elem, fromD, toD) { + fromD = fromD || ''; + var shift = elem.shift, + bezier = fromD.indexOf('C') > -1, + numParams = bezier ? 7 : 3, + endLength, + slice, + i, + start = fromD.split(' '), + end = [].concat(toD), // copy + startBaseLine, + endBaseLine, + sixify = function(arr) { // in splines make move points have six parameters like bezier curves + i = arr.length; + while (i--) { + if (arr[i] == M) { + arr.splice(i + 1, 0, arr[i+1], arr[i+2], arr[i+1], arr[i+2]); + } + } + }; + + if (bezier) { + sixify(start); + sixify(end); + } + + // pull out the base lines before padding + if (elem.isArea) { + startBaseLine = start.splice(start.length - 6, 6); + endBaseLine = end.splice(end.length - 6, 6); + } + + // if shifting points, prepend a dummy point to the end path + if (shift) { + + end = [].concat(end).splice(0, numParams).concat(end); + elem.shift = false; // reset for following animations + } + + // copy and append last point until the length matches the end length + if (start.length) { + endLength = end.length; + while (start.length < endLength) { + + //bezier && sixify(start); + slice = [].concat(start).splice(start.length - numParams, numParams); + if (bezier) { // disable first control point + slice[numParams - 6] = slice[numParams - 2]; + slice[numParams - 5] = slice[numParams - 1]; + } + start = start.concat(slice); + } + } + + if (startBaseLine) { // append the base lines for areas + start = start.concat(startBaseLine); + end = end.concat(endBaseLine); + } + return [start, end]; + }, + + /** + * Interpolate each value of the path and return the array + */ + step: function(start, end, pos, complete) { + var ret = [], + i = start.length, + startVal; + + if (pos == 1) { // land on the final path without adjustment points appended in the ends + ret = complete; + + } else if (i == end.length && pos < 1) { + while (i--) { + startVal = parseFloat(start[i]); + ret[i] = + isNaN(startVal) ? // a letter instruction like M or L + start[i] : + pos * (parseFloat(end[i] - startVal)) + startVal; + + } + } else { // if animation is finished or length not matching, land on right value + ret = end; + } + return ret; + } +}; + +/** + * Set the time methods globally based on the useUTC option. Time method can be either + * local time or UTC (default). + */ +function setTimeMethods() { + var useUTC = defaultOptions.global.useUTC; + + makeTime = useUTC ? Date.UTC : function(year, month, date, hours, minutes, seconds) { + return new Date( + year, + month, + pick(date, 1), + pick(hours, 0), + pick(minutes, 0), + pick(seconds, 0) + ).getTime(); + }; + getMinutes = useUTC ? 'getUTCMinutes' : 'getMinutes'; + getHours = useUTC ? 'getUTCHours' : 'getHours'; + getDay = useUTC ? 'getUTCDay' : 'getDay'; + getDate = useUTC ? 'getUTCDate' : 'getDate'; + getMonth = useUTC ? 'getUTCMonth' : 'getMonth'; + getFullYear = useUTC ? 'getUTCFullYear' : 'getFullYear'; + setMinutes = useUTC ? 'setUTCMinutes' : 'setMinutes'; + setHours = useUTC ? 'setUTCHours' : 'setHours'; + setDate = useUTC ? 'setUTCDate' : 'setDate'; + setMonth = useUTC ? 'setUTCMonth' : 'setMonth'; + setFullYear = useUTC ? 'setUTCFullYear' : 'setFullYear'; + +} + +/** + * Merge the default options with custom options and return the new options structure + * @param {Object} options The new custom options + */ +function setOptions(options) { + defaultOptions = merge(defaultOptions, options); + + // apply UTC + setTimeMethods(); + + return defaultOptions; +} + +/** + * Get the updated default options. Merely exposing defaultOptions for outside modules + * isn't enough because the setOptions method creates a new object. + */ +function getOptions() { + return defaultOptions; +} + +/** + * Discard an element by moving it to the bin and delete + * @param {Object} The HTML node to discard + */ +function discardElement(element) { + // create a garbage bin element, not part of the DOM + if (!garbageBin) { + garbageBin = createElement(DIV); + } + + // move the node and empty bin + if (element) { + garbageBin.appendChild(element); + } + garbageBin.innerHTML = ''; +} + +/* **************************************************************************** + * Handle the options * + *****************************************************************************/ +var + +defaultLabelOptions = { + enabled: true, + // rotation: 0, + align: 'center', + x: 0, + y: 15, + /*formatter: function() { + return this.value; + },*/ + style: { + color: '#666', + fontSize: '11px', + lineHeight: '14px' + } +}; + +defaultOptions = { + colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE', + '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92'], + symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'], + lang: { + loading: 'Loading...', + months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December'], + weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + decimalPoint: '.', + resetZoom: 'Reset zoom', + resetZoomTitle: 'Reset zoom level 1:1', + thousandsSep: ',' + }, + global: { + useUTC: true + }, + chart: { + //animation: true, + //alignTicks: false, + //reflow: true, + //className: null, + //events: { load, selection }, + //margin: [null], + //marginTop: null, + //marginRight: null, + //marginBottom: null, + //marginLeft: null, + borderColor: '#4572A7', + //borderWidth: 0, + borderRadius: 5, + defaultSeriesType: 'line', + ignoreHiddenSeries: true, + //inverted: false, + //shadow: false, + spacingTop: 10, + spacingRight: 10, + spacingBottom: 15, + spacingLeft: 10, + style: { + fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font + fontSize: '12px' + }, + backgroundColor: '#FFFFFF', + //plotBackgroundColor: null, + plotBorderColor: '#C0C0C0' + //plotBorderWidth: 0, + //plotShadow: false, + //zoomType: '' + }, + title: { + text: 'Chart title', + align: 'center', + // floating: false, + // margin: 15, + // x: 0, + // verticalAlign: 'top', + y: 15, // docs + style: { + color: '#3E576F', + fontSize: '16px' + } + + }, + subtitle: { + text: '', + align: 'center', + // floating: false + // x: 0, + // verticalAlign: 'top', + y: 30, // docs + style: { + color: '#6D869F' + } + }, + + plotOptions: { + line: { // base series options + allowPointSelect: false, + showCheckbox: false, + animation: { + duration: 1000 + }, + //cursor: 'default', + //dashStyle: null, + //enableMouseTracking: true, + events: {}, + lineWidth: 2, + shadow: true, + // stacking: null, + marker: { + enabled: true, + //symbol: null, + lineWidth: 0, + radius: 4, + lineColor: '#FFFFFF', + //fillColor: null, + states: { // states for a single point + hover: { + //radius: base + 2 + }, + select: { + fillColor: '#FFFFFF', + lineColor: '#000000', + lineWidth: 2 + } + } + }, + point: { + events: {} + }, + dataLabels: merge(defaultLabelOptions, { + enabled: false, + y: -6, + formatter: function() { + return this.y; + } + }), + + //pointStart: 0, + //pointInterval: 1, + showInLegend: true, + states: { // states for the entire series + hover: { + //enabled: false, + //lineWidth: base + 1, + marker: { + // lineWidth: base + 1, + // radius: base + 1 + } + }, + select: { + marker: {} + } + }, + stickyTracking: true + //zIndex: null + } + }, + labels: { + //items: [], + style: { + //font: defaultFont, + position: ABSOLUTE, + color: '#3E576F' + } + }, + legend: { + enabled: true, + align: 'center', + //floating: false, + layout: 'horizontal', + labelFormatter: function() { + return this.name; + }, + // lineHeight: 16, // docs: deprecated + borderWidth: 1, + borderColor: '#909090', + borderRadius: 5, + // margin: 10, + // reversed: false, + shadow: false, + // backgroundColor: null, + style: { + padding: '5px' + }, + itemStyle: { + cursor: 'pointer', + color: '#3E576F' + }, + itemHoverStyle: { + cursor: 'pointer', + color: '#000000' + }, + itemHiddenStyle: { + color: '#C0C0C0' + }, + itemCheckboxStyle: { + position: ABSOLUTE, + width: '13px', // for IE precision + height: '13px' + }, + // itemWidth: undefined, + symbolWidth: 16, + symbolPadding: 5, + verticalAlign: 'bottom', + // width: undefined, + x: 0, // docs + y: 0 // docs + }, + + loading: { + hideDuration: 100, + labelStyle: { + fontWeight: 'bold', + position: RELATIVE, + top: '1em' + }, + showDuration: 100, + style: { + position: ABSOLUTE, + backgroundColor: 'white', + opacity: 0.5, + textAlign: 'center' + } + }, + + tooltip: { + enabled: true, + //crosshairs: null, + backgroundColor: 'rgba(255, 255, 255, .85)', + borderWidth: 2, + borderRadius: 5, + //formatter: defaultFormatter, + shadow: true, + //shared: false, + snap: hasTouch ? 25 : 10, + style: { + color: '#333333', + fontSize: '12px', + padding: '5px', + whiteSpace: 'nowrap' + } + }, + + toolbar: { + itemStyle: { + color: '#4572A7', + cursor: 'pointer' + } + }, + + credits: { + enabled: true, + text: 'Highcharts.com', + href: 'http://www.highcharts.com', + position: { + align: 'right', + x: -10, + verticalAlign: 'bottom', + y: -5 + }, + style: { + cursor: 'pointer', + color: '#909090', + fontSize: '10px' + } + } +}; + +// Axis defaults +var defaultXAxisOptions = { + // allowDecimals: null, + // alternateGridColor: null, + // categories: [], + dateTimeLabelFormats: { + second: '%H:%M:%S', + minute: '%H:%M', + hour: '%H:%M', + day: '%e. %b', + week: '%e. %b', + month: '%b \'%y', + year: '%Y' + }, + endOnTick: false, + gridLineColor: '#C0C0C0', + // gridLineDashStyle: 'solid', // docs + // gridLineWidth: 0, + // reversed: false, + + labels: defaultLabelOptions, + // { step: null }, + lineColor: '#C0D0E0', + lineWidth: 1, + //linkedTo: null, + max: null, + min: null, + minPadding: 0.01, + maxPadding: 0.01, + //maxZoom: null, + minorGridLineColor: '#E0E0E0', + // minorGridLineDashStyle: null, + minorGridLineWidth: 1, + minorTickColor: '#A0A0A0', + //minorTickInterval: null, + minorTickLength: 2, + minorTickPosition: 'outside', // inside or outside + //minorTickWidth: 0, + //opposite: false, + //offset: 0, + //plotBands: [{ + // events: {}, + // zIndex: 1, + // labels: { align, x, verticalAlign, y, style, rotation, textAlign } + //}], + //plotLines: [{ + // events: {} + // dashStyle: {} + // zIndex: + // labels: { align, x, verticalAlign, y, style, rotation, textAlign } + //}], + //reversed: false, + // showFirstLabel: true, + // showLastLabel: false, + startOfWeek: 1, + startOnTick: false, + tickColor: '#C0D0E0', + //tickInterval: null, + tickLength: 5, + tickmarkPlacement: 'between', // on or between + tickPixelInterval: 100, + tickPosition: 'outside', + tickWidth: 1, + title: { + //text: null, + align: 'middle', // low, middle or high + //margin: 0 for horizontal, 10 for vertical axes, + //rotation: 0, + //side: 'outside', + style: { + color: '#6D869F', + //font: defaultFont.replace('normal', 'bold') + fontWeight: 'bold' + } + //x: 0, + //y: 0 + }, + type: 'linear' // linear or datetime +}, + +defaultYAxisOptions = merge(defaultXAxisOptions, { + endOnTick: true, + gridLineWidth: 1, + tickPixelInterval: 72, + showLastLabel: true, + labels: { + align: 'right', + x: -8, + y: 3 + }, + lineWidth: 0, + maxPadding: 0.05, + minPadding: 0.05, + startOnTick: true, + tickWidth: 0, + title: { + rotation: 270, + text: 'Y-values' + } +}), + +defaultLeftAxisOptions = { + labels: { + align: 'right', + x: -8, + y: null // docs + }, + title: { + rotation: 270 + } +}, +defaultRightAxisOptions = { + labels: { + align: 'left', + x: 8, + y: null // docs + }, + title: { + rotation: 90 + } +}, +defaultBottomAxisOptions = { // horizontal axis + labels: { + align: 'center', + x: 0, + y: 14 + // staggerLines: null + }, + title: { + rotation: 0 + } +}, +defaultTopAxisOptions = merge(defaultBottomAxisOptions, { + labels: { + y: -5 + // staggerLines: null + } +}); + + + + +// Series defaults +var defaultPlotOptions = defaultOptions.plotOptions, + defaultSeriesOptions = defaultPlotOptions.line; +//defaultPlotOptions.line = merge(defaultSeriesOptions); +defaultPlotOptions.spline = merge(defaultSeriesOptions); +defaultPlotOptions.scatter = merge(defaultSeriesOptions, { + lineWidth: 0, + states: { + hover: { + lineWidth: 0 + } + } +}); +defaultPlotOptions.area = merge(defaultSeriesOptions, { + // threshold: 0, + // lineColor: null, // overrides color, but lets fillColor be unaltered + // fillOpacity: 0.75, + // fillColor: null + +}); +defaultPlotOptions.areaspline = merge(defaultPlotOptions.area); +defaultPlotOptions.column = merge(defaultSeriesOptions, { + borderColor: '#FFFFFF', + borderWidth: 1, + borderRadius: 0, + //colorByPoint: undefined, + groupPadding: 0.2, + marker: null, // point options are specified in the base options + pointPadding: 0.1, + //pointWidth: null, + minPointLength: 0, + states: { + hover: { + brightness: 0.1, + shadow: false + }, + select: { + color: '#C0C0C0', + borderColor: '#000000', + shadow: false + } + } +}); +defaultPlotOptions.bar = merge(defaultPlotOptions.column, { + dataLabels: { + align: 'left', + x: 5, + y: 0 + } +}); +defaultPlotOptions.pie = merge(defaultSeriesOptions, { + //dragType: '', // n/a + borderColor: '#FFFFFF', + borderWidth: 1, + center: ['50%', '50%'], + colorByPoint: true, // always true for pies + dataLabels: { + // align: null, + // connectorWidth: 1, + // connectorColor: '#606060', + // connectorPadding: 5, + distance: 30, + enabled: true, + formatter: function() { + return this.point.name; + }, + y: 5 + }, + //innerSize: 0, + legendType: 'point', + marker: null, // point options are specified in the base options + size: '75%', + showInLegend: false, + slicedOffset: 10, + states: { + hover: { + brightness: 0.1, + shadow: false + } + } + +}); + +// set the default time methods +setTimeMethods(); + + +/** + * Extend a prototyped class by new members + * @param {Object} parent + * @param {Object} members + */ +function extendClass(parent, members) { + var object = function(){}; + object.prototype = new parent(); + extend(object.prototype, members); + return object; +} + + +/** + * Handle color operations. The object methods are chainable. + * @param {String} input The input color in either rbga or hex format + */ +var Color = function(input) { + // declare variables + var rgba = [], result; + + /** + * Parse the input color to rgba array + * @param {String} input + */ + function init(input) { + + // rgba + if((result = /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(input))) { + rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)]; + } + + // hex + else if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(input))) { + rgba = [pInt(result[1],16), pInt(result[2],16), pInt(result[3],16), 1]; + } + + } + /** + * Return the color a specified format + * @param {String} format + */ + function get(format) { + var ret; + + // it's NaN if gradient colors on a column chart + if (rgba && !isNaN(rgba[0])) { + if (format == 'rgb') { + ret = 'rgb('+ rgba[0] +','+ rgba[1] +','+ rgba[2] +')'; + } else if (format == 'a') { + ret = rgba[3]; + } else { + ret = 'rgba('+ rgba.join(',') +')'; + } + } else { + ret = input; + } + return ret; + } + + /** + * Brighten the color + * @param {Number} alpha + */ + function brighten(alpha) { + if (isNumber(alpha) && alpha !== 0) { + var i; + for (i = 0; i < 3; i++) { + rgba[i] += pInt(alpha * 255); + + if (rgba[i] < 0) { + rgba[i] = 0; + } + if (rgba[i] > 255) { + rgba[i] = 255; + } + } + } + return this; + } + /** + * Set the color's opacity to a given alpha value + * @param {Number} alpha + */ + function setOpacity(alpha) { + rgba[3] = alpha; + return this; + } + + // initialize: parse the input + init(input); + + // public methods + return { + get: get, + brighten: brighten, + setOpacity: setOpacity + }; +}; + + + +/** + * Format a number and return a string based on input settings + * @param {Number} number The input number to format + * @param {Number} decimals The amount of decimals + * @param {String} decPoint The decimal point, defaults to the one given in the lang options + * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options + */ +function numberFormat (number, decimals, decPoint, thousandsSep) { + var lang = defaultOptions.lang, + // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/ + n = number, c = isNaN(decimals = mathAbs(decimals)) ? 2 : decimals, + d = decPoint === undefined ? lang.decimalPoint : decPoint, + t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep, s = n < 0 ? "-" : "", + i = pInt(n = mathAbs(+n || 0).toFixed(c)) + "", j = (j = i.length) > 3 ? j % 3 : 0; + + return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + + (c ? d + mathAbs(n - i).toFixed(c).slice(2) : ""); +} + +/** + * Based on http://www.php.net/manual/en/function.strftime.php + * @param {String} format + * @param {Number} timestamp + * @param {Boolean} capitalize + */ +dateFormat = function (format, timestamp, capitalize) { + function pad (number) { + return number.toString().replace(/^([0-9])$/, '0$1'); + } + + if (!defined(timestamp) || isNaN(timestamp)) { + return 'Invalid date'; + } + format = pick(format, '%Y-%m-%d %H:%M:%S'); + + var date = new Date(timestamp * timeFactor), + + // get the basic time values + hours = date[getHours](), + day = date[getDay](), + dayOfMonth = date[getDate](), + month = date[getMonth](), + fullYear = date[getFullYear](), + lang = defaultOptions.lang, + langWeekdays = lang.weekdays, + langMonths = lang.months, + + // list all format keys + replacements = { + + // Day + 'a': langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon' + 'A': langWeekdays[day], // Long weekday, like 'Monday' + 'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31 + 'e': dayOfMonth, // Day of the month, 1 through 31 + + // Week (none implemented) + + // Month + 'b': langMonths[month].substr(0, 3), // Short month, like 'Jan' + 'B': langMonths[month], // Long month, like 'January' + 'm': pad(month + 1), // Two digit month number, 01 through 12 + + // Year + 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009 + 'Y': fullYear, // Four digits year, like 2009 + + // Time + 'H': pad(hours), // Two digits hours in 24h format, 00 through 23 + 'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11 + 'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12 + 'M': pad(date[getMinutes]()), // Two digits minutes, 00 through 59 + 'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM + 'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM + 'S': pad(date.getSeconds()) // Two digits seconds, 00 through 59 + + }; + + + // do the replaces + for (var key in replacements) { + format = format.replace('%'+ key, replacements[key]); + } + + // Optionally capitalize the string and return + return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format; +}; + + + +/** + * Loop up the node tree and add offsetWidth and offsetHeight to get the + * total page offset for a given element. Used by Opera and iOS on hover and + * all browsers on point click. + * + * @param {Object} el + * + */ +function getPosition (el) { + var p = { left: el.offsetLeft, top: el.offsetTop }; + while ((el = el.offsetParent)) { + p.left += el.offsetLeft; + p.top += el.offsetTop; + if (el != doc.body && el != doc.documentElement) { + p.left -= el.scrollLeft; + p.top -= el.scrollTop; + } + } + return p; +} + + +/** + * A wrapper object for SVG elements + */ +function SVGElement () {} + +SVGElement.prototype = { + /** + * Initialize the SVG renderer + * @param {Object} renderer + * @param {String} nodeName + */ + init: function(renderer, nodeName) { + this.element = doc.createElementNS(SVG_NS, nodeName); + this.renderer = renderer; + }, + /** + * Animate a given attribute + * @param {Object} params + * @param {Number} options The same options as in jQuery animation + * @param {Function} complete Function to perform at the end of animation + */ + animate: function(params, options, complete) { + var animOptions = pick(options, globalAnimation, true); + if (animOptions) { + animOptions = merge(animOptions); + if (complete) { // allows using a callback with the global animation without overwriting it + animOptions.complete = complete; + } + animate(this, params, animOptions); + } else { + this.attr(params); + if (complete) { + complete(); + } + } + }, + /** + * Set or get a given attribute + * @param {Object|String} hash + * @param {Mixed|Undefined} val + */ + attr: function(hash, val) { + var key, + value, + i, + child, + element = this.element, + nodeName = element.nodeName, + renderer = this.renderer, + skipAttr, + shadows = this.shadows, + hasSetSymbolSize, + ret = this; + + // single key-value pair + if (isString(hash) && defined(val)) { + key = hash; + hash = {}; + hash[key] = val; + } + + // used as a getter: first argument is a string, second is undefined + if (isString(hash)) { + key = hash; + if (nodeName == 'circle') { + key = { x: 'cx', y: 'cy' }[key] || key; + } else if (key == 'strokeWidth') { + key = 'stroke-width'; + } + ret = attr(element, key) || this[key] || 0; + + if (key != 'd' && key != 'visibility') { // 'd' is string in animation step + ret = parseFloat(ret); + } + + // setter + } else { + + for (key in hash) { + skipAttr = false; // reset + value = hash[key]; + + // paths + if (key == 'd') { + if (value && value.join) { // join path + value = value.join(' '); + } + if (/(NaN| {2}|^$)/.test(value)) { + value = 'M 0 0'; + } + this.d = value; // shortcut for animations + + // update child tspans x values + } else if (key == 'x' && nodeName == 'text') { + for (i = 0; i < element.childNodes.length; i++ ) { + child = element.childNodes[i]; + // if the x values are equal, the tspan represents a linebreak + if (attr(child, 'x') == attr(element, 'x')) { + //child.setAttribute('x', value); + attr(child, 'x', value); + } + } + + if (this.rotation) { + attr(element, 'transform', 'rotate('+ this.rotation +' '+ value +' '+ + pInt(hash.y || attr(element, 'y')) +')'); + } + + // apply gradients + } else if (key == 'fill') { + value = renderer.color(value, element, key); + + // circle x and y + } else if (nodeName == 'circle' && (key == 'x' || key == 'y')) { + key = { x: 'cx', y: 'cy' }[key] || key; + + // translation and text rotation + } else if (key == 'translateX' || key == 'translateY' || key == 'rotation' || key == 'verticalAlign') { + this[key] = value; + this.updateTransform(); + skipAttr = true; + + // apply opacity as subnode (required by legacy WebKit and Batik) + } else if (key == 'stroke') { + value = renderer.color(value, element, key); + + // emulate VML's dashstyle implementation + } else if (key == 'dashstyle') { + key = 'stroke-dasharray'; + if (value) { + value = value.toLowerCase() + .replace('shortdashdotdot', '3,1,1,1,1,1,') + .replace('shortdashdot', '3,1,1,1') + .replace('shortdot', '1,1,') + .replace('shortdash', '3,1,') + .replace('longdash', '8,3,') + .replace(/dot/g, '1,3,') + .replace('dash', '4,3,') + .replace(/,$/, '') + .split(','); // ending comma + + i = value.length; + while (i--) { + value[i] = pInt(value[i]) * hash['stroke-width']; + } + value = value.join(','); + } + + // special + } else if (key == 'isTracker') { + this[key] = value; + + // IE9/MooTools combo: MooTools returns objects instead of numbers and IE9 Beta 2 + // is unable to cast them. Test again with final IE9. + } else if (key == 'width') { + value = pInt(value); + + // Text alignment + } else if (key == 'align') { + key = 'text-anchor'; + value = { left: 'start', center: 'middle', right: 'end' }[value]; + } + + + + // jQuery animate changes case + if (key == 'strokeWidth') { + key = 'stroke-width'; + } + + // Chrome/Win < 6 bug (http://code.google.com/p/chromium/issues/detail?id=15461) + if (isWebKit && key == 'stroke-width' && value === 0) { + value = 0.000001; + } + + // symbols + if (this.symbolName && /^(x|y|r|start|end|innerR)/.test(key)) { + + + if (!hasSetSymbolSize) { + this.symbolAttr(hash); + hasSetSymbolSize = true; + } + skipAttr = true; + } + + // let the shadow follow the main element + if (shadows && /^(width|height|visibility|x|y|d)$/.test(key)) { + i = shadows.length; + while (i--) { + attr(shadows[i], key, value); + } + } + + /* trows errors in Chrome + if ((key == 'width' || key == 'height') && nodeName == 'rect' && value < 0) { + console.log(element); + } + */ + + + + if (key == 'text') { + // only one node allowed + this.textStr = value; + if (this.added) { + renderer.buildText(this); + } + } else if (!skipAttr) { + //element.setAttribute(key, value); + attr(element, key, value); + } + + } + + } + return ret; + }, + + /** + * If one of the symbol size affecting parameters are changed, + * check all the others only once for each call to an element's + * .attr() method + * @param {Object} hash + */ + symbolAttr: function(hash) { + var wrapper = this; + + each (['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR'], function(key) { + wrapper[key] = pick(hash[key], wrapper[key]); + }); + + wrapper.attr({ + d: wrapper.renderer.symbols[wrapper.symbolName](wrapper.x, wrapper.y, wrapper.r, { + start: wrapper.start, + end: wrapper.end, + width: wrapper.width, + height: wrapper.height, + innerR: wrapper.innerR + }) + }); + }, + + /** + * Apply a clipping path to this object + * @param {String} id + */ + clip: function(clipRect) { + return this.attr('clip-path', 'url('+ this.renderer.url +'#'+ clipRect.id +')'); + }, + + /** + * Calculate the coordinates needed for drawing a rectangle crisply and return the + * calculated attributes + * @param {Number} strokeWidth + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + crisp: function(strokeWidth, x, y, width, height) { + + var wrapper = this, + key, + attr = {}, + values = {}, + normalizer; + + strokeWidth = strokeWidth || wrapper.strokeWidth || 0; + normalizer = strokeWidth % 2 / 2; + + // normalize for crisp edges + values.x = mathFloor(x || wrapper.x || 0) + normalizer; + values.y = mathFloor(y || wrapper.y || 0) + normalizer; + values.width = mathFloor((width || wrapper.width || 0) - 2 * normalizer); + values.height = mathFloor((height || wrapper.height || 0) - 2 * normalizer); + values.strokeWidth = strokeWidth; + + for (key in values) { + if (wrapper[key] != values[key]) { // only set attribute if changed + wrapper[key] = attr[key] = values[key]; + } + } + + return attr; + }, + + /** + * Set styles for the element + * @param {Object} styles + */ + css: function(styles) { + var elemWrapper = this, + elem = elemWrapper.element, + textWidth = styles && styles.width && elem.nodeName == 'text'; + + // convert legacy + if (styles && styles.color) { + styles.fill = styles.color; + } + + // save the styles in an object + styles = extend( + elemWrapper.styles, + styles + ); + + + // store object + elemWrapper.styles = styles; + + // serialize and set style attribute + if (isIE && !hasSVG) { // legacy IE doesn't support setting style attribute + if (textWidth) { + delete styles.width; + } + css(elemWrapper.element, styles); + } else { + elemWrapper.attr({ + style: serializeCSS(styles) + }); + } + + + // re-build text + if (textWidth && elemWrapper.added) { + elemWrapper.renderer.buildText(elemWrapper); + } + + return elemWrapper; + }, + + /** + * Add an event listener + * @param {String} eventType + * @param {Function} handler + */ + on: function(eventType, handler) { + var fn = handler; + // touch + if (hasTouch && eventType == 'click') { + eventType = 'touchstart'; + fn = function(e) { + e.preventDefault(); + handler(); + } + } + // simplest possible event model for internal use + this.element['on'+ eventType] = fn; + return this; + }, + + + /** + * Move an object and its children by x and y values + * @param {Number} x + * @param {Number} y + */ + translate: function(x, y) { + return this.attr({ + translateX: x, + translateY: y + }); + }, + + /** + * Invert a group, rotate and flip + */ + invert: function() { + var wrapper = this; + wrapper.inverted = true; + wrapper.updateTransform(); + return wrapper; + }, + + /** + * Private method to update the transform attribute based on internal + * properties + */ + updateTransform: function() { + var wrapper = this, + translateX = wrapper.translateX || 0, + translateY = wrapper.translateY || 0, + inverted = wrapper.inverted, + rotation = wrapper.rotation, + transform = []; + + // flipping affects translate as adjustment for flipping around the group's axis + if (inverted) { + translateX += wrapper.attr('width'); + translateY += wrapper.attr('height'); + } + + // apply translate + if (translateX || translateY) { + transform.push('translate('+ translateX +','+ translateY +')'); + } + + // apply rotation + if (inverted) { + transform.push('rotate(90) scale(-1,1)'); + } else if (rotation) { // text rotation + transform.push('rotate('+ rotation +' '+ wrapper.x +' '+ wrapper.y +')'); + } + + if (transform.length) { + attr(wrapper.element, 'transform', transform.join(' ')); + } + }, + /** + * Bring the element to the front + */ + toFront: function() { + var element = this.element; + element.parentNode.appendChild(element); + return this; + }, + + + /** + * Break down alignment options like align, verticalAlign, x and y + * to x and y relative to the chart. + * + * @param {Object} alignOptions + * @param {Boolean} alignByTranslate + * @param {Object} box The box to align to, needs a width and height + * + */ + align: function(alignOptions, alignByTranslate, box) { + + if (!alignOptions) { // called on resize + alignOptions = this.alignOptions; + alignByTranslate = this.alignByTranslate; + } else { // first call on instanciate + this.alignOptions = alignOptions; + this.alignByTranslate = alignByTranslate; + if (!box) { // boxes other than renderer handle this internally + this.renderer.alignedObjects.push(this); + } + } + + box = pick(box, this.renderer); + + var align = alignOptions.align, + vAlign = alignOptions.verticalAlign, + x = (box.x || 0) + (alignOptions.x || 0), // default: left align + y = (box.y || 0) + (alignOptions.y || 0), // default: top align + attribs = {}; + + + // align + if (/^(right|center)$/.test(align)) { + x += (box.width - (alignOptions.width || 0) ) / + { right: 1, center: 2 }[align]; + } + attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x); + + + // vertical align + if (/^(bottom|middle)$/.test(vAlign)) { + y += (box.height - (alignOptions.height || 0)) / + ({ bottom: 1, middle: 2 }[vAlign] || 1); + + } + attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y); + + // animate only if already placed + this[this.placed ? 'animate' : 'attr'](attribs); + this.placed = true; + + return this; + }, + + /** + * Get the bounding box (width, height, x and y) for the element + */ + getBBox: function() { + var bBox, + width, + height, + rotation = this.rotation, + rad = rotation * deg2rad; + + try { // fails in Firefox if the container has display: none + // use extend because IE9 is not allowed to change width and height in case + // of rotation (below) + bBox = extend({}, this.element.getBBox()); + } catch(e) { + bBox = { width: 0, height: 0 }; + } + width = bBox.width; + height = bBox.height; + + // adjust for rotated text + if (rotation) { + bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad)); + bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad)); + } + + return bBox; + }, + + /* * + * Manually compute width and height of rotated text from non-rotated. Shared by SVG and VML + * @param {Object} bBox + * @param {number} rotation + * / + rotateBBox: function(bBox, rotation) { + var rad = rotation * math.PI * 2 / 360, // radians + width = bBox.width, + height = bBox.height; + + + },*/ + + /** + * Show the element + */ + show: function() { + return this.attr({ visibility: VISIBLE }); + }, + + /** + * Hide the element + */ + hide: function() { + return this.attr({ visibility: HIDDEN }); + }, + + /** + * Add the element + * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined + * to append the element to the renderer.box. + */ + add: function(parent) { + + var renderer = this.renderer, + parentWrapper = parent || renderer, + parentNode = parentWrapper.element || renderer.box, + childNodes = parentNode.childNodes, + element = this.element, + zIndex = attr(element, 'zIndex'), + otherElement, + otherZIndex, + i; + + // mark as inverted + this.parentInverted = parent && parent.inverted; + + // build formatted text + if (this.textStr !== undefined) { + renderer.buildText(this); + } + + // mark the container as having z indexed children + if (zIndex) { + parentWrapper.handleZ = true; + zIndex = pInt(zIndex); + } + + // insert according to this and other elements' zIndex + if (parentWrapper.handleZ) { // this element or any of its siblings has a z index + for (i = 0; i < childNodes.length; i++) { + otherElement = childNodes[i]; + otherZIndex = attr(otherElement, 'zIndex'); + if (otherElement != element && ( + // insert before the first element with a higher zIndex + pInt(otherZIndex) > zIndex || + // if no zIndex given, insert before the first element with a zIndex + (!defined(zIndex) && defined(otherZIndex)) + + )) { + parentNode.insertBefore(element, otherElement); + return this; + } + } + } + + // default: append at the end + parentNode.appendChild(element); + + this.added = true; + + return this; + }, + + /** + * Destroy the element and element wrapper + */ + destroy: function() { + var wrapper = this, + element = wrapper.element || {}, + shadows = wrapper.shadows, + parentNode = element.parentNode, + key; + + // remove events + element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = null; + stop(wrapper); // stop running animations + + // remove element + if (parentNode) { + parentNode.removeChild(element); + } + + // destroy shadows + if (shadows) { + each(shadows, function(shadow) { + parentNode = shadow.parentNode; + if (parentNode) { // the entire chart HTML can be overwritten + parentNode.removeChild(shadow); + } + }); + } + + // remove from alignObjects + erase(wrapper.renderer.alignedObjects, wrapper); + + for (key in wrapper) { + delete wrapper[key]; + } + + return null; + }, + + /** + * Empty a group element + */ + empty: function() { + var element = this.element, + childNodes = element.childNodes, + i = childNodes.length; + + while (i--) { + element.removeChild(childNodes[i]); + } + }, + + /** + * Add a shadow to the element. Must be done after the element is added to the DOM + * @param {Boolean} apply + */ + shadow: function(apply) { + var shadows = [], + i, + shadow, + element = this.element, + + // compensate for inverted plot area + transform = this.parentInverted ? '(-1,-1)' : '(1,1)'; + + + if (apply) { + for (i = 1; i <= 3; i++) { + shadow = element.cloneNode(0); + attr(shadow, { + 'isShadow': 'true', + 'stroke': 'rgb(0, 0, 0)', + 'stroke-opacity': 0.05 * i, + 'stroke-width': 7 - 2 * i, + 'transform': 'translate'+ transform, + 'fill': NONE + }); + + + element.parentNode.insertBefore(shadow, element); + + shadows.push(shadow); + } + + this.shadows = shadows; + } + return this; + + } +}; + + + +/** + * The default SVG renderer + */ +var SVGRenderer = function() { + this.init.apply(this, arguments); +}; +SVGRenderer.prototype = { + /** + * Initialize the SVGRenderer + * @param {Object} container + * @param {Number} width + * @param {Number} height + * @param {Boolean} forExport + */ + init: function(container, width, height, forExport) { + var renderer = this, + loc = location, + boxWrapper; + + renderer.Element = SVGElement; + boxWrapper = renderer.createElement('svg') + .attr({ + xmlns: SVG_NS, + version: '1.1' + }); + container.appendChild(boxWrapper.element); + + // object properties + renderer.box = boxWrapper.element; + renderer.boxWrapper = boxWrapper; + renderer.alignedObjects = []; + renderer.url = isIE ? '' : loc.href.replace(/#.*?$/, ''); // page url used for internal references + renderer.defs = this.createElement('defs').add(); + renderer.forExport = forExport; + + renderer.setSize(width, height, false); + + }, + + + /** + * Create a wrapper for an SVG element + * @param {Object} nodeName + */ + createElement: function(nodeName) { + var wrapper = new this.Element(); + wrapper.init(this, nodeName); + return wrapper; + }, + + + /** + * Parse a simple HTML string into SVG tspans + * + * @param {Object} textNode The parent text SVG node + */ + buildText: function(wrapper) { + var textNode = wrapper.element, + lines = pick(wrapper.textStr, '').toString() + .replace(/<(b|strong)>/g, '') + .replace(/<(i|em)>/g, '') + .replace(//g, '') + .split(/]?>/g), + childNodes = textNode.childNodes, + styleRegex = /style="([^"]+)"/, + hrefRegex = /href="([^"]+)"/, + parentX = attr(textNode, 'x'), + textStyles = wrapper.styles, + reverse = isFirefox && textStyles && textStyles.HcDirection == 'rtl' && !this.forExport, // issue #38 + arr, + width = textStyles && pInt(textStyles.width), + textLineHeight = textStyles && textStyles.lineHeight, + lastLine, + i = childNodes.length; + + // remove old text + while (i--) { + textNode.removeChild(childNodes[i]); + } + + if (width && !wrapper.added) { + this.box.appendChild(textNode); // attach it to the DOM to read offset width + } + + each(lines, function(line, lineNo) { + var spans, spanNo = 0, lineHeight; + + line = line.replace(//g, '|||'); + spans = line.split('|||'); + + each(spans, function (span) { + if (span !== '' || spans.length == 1) { + var attributes = {}, + tspan = doc.createElementNS(SVG_NS, 'tspan'); + if (styleRegex.test(span)) { + attr( + tspan, + 'style', + span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2') + ); + } + if (hrefRegex.test(span)) { + attr(tspan, 'onclick', 'location.href=\"'+ span.match(hrefRegex)[1] +'\"'); + css(tspan, { cursor: 'pointer' }); + } + + span = span.replace(/<(.|\n)*?>/g, '') || ' '; + + // issue #38 workaround. + if (reverse) { + arr = []; + i = span.length; + while (i--) { + arr.push(span.charAt(i)) + } + span = arr.join(''); + } + + // add the text node + tspan.appendChild(doc.createTextNode(span)); + + if (!spanNo) { // first span in a line, align it to the left + attributes.x = parentX; + } else { + // Firefox ignores spaces at the front or end of the tspan + attributes.dx = 3; // space + } + + // first span on subsequent line, add the line height + if (!spanNo) { + if (lineNo) { + // Webkit and opera sometimes return 'normal' as the line height. In that + // case, webkit uses offsetHeight, while Opera falls back to 18 + lineHeight = pInt(window.getComputedStyle(lastLine, null).getPropertyValue('line-height')); + if (isNaN(lineHeight)) { + lineHeight = textLineHeight || lastLine.offsetHeight || 18; + } + attr(tspan, 'dy', lineHeight); + } + lastLine = tspan; // record for use in next line + } + + // add attributes + attr(tspan, attributes); + + // append it + textNode.appendChild(tspan); + + spanNo++; + + // check width and apply soft breaks + if (width) { + var words = span.replace(/-/g, '- ').split(' '), + tooLong, + actualWidth, + rest = []; + + while (words.length || rest.length) { + actualWidth = textNode.getBBox().width; + tooLong = actualWidth > width; + if (!tooLong || words.length == 1) { // new line needed + words = rest; + rest = []; + if (words.length) { + tspan = doc.createElementNS(SVG_NS, 'tspan'); + attr(tspan, { + x: parentX, + dy: textLineHeight || 16 + }); + textNode.appendChild(tspan); + + if (actualWidth > width) { // a single word is pressing it out + width = actualWidth; + } + } + } else { // append to existing line tspan + tspan.removeChild(tspan.firstChild); + rest.unshift(words.pop()); + } + + tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-'))); + } + + } + } + }); + }); + + + }, + + /** + * Make a straight line crisper by not spilling out to neighbour pixels + * @param {Array} points + * @param {Number} width + */ + crispLine: function(points, width) { + // points format: [M, 0, 0, L, 100, 0] + // normalize to a crisp line + if (points[1] == points[4]) { + points[1] = points[4] = mathRound(points[1]) + (width % 2 / 2); + } + if (points[2] == points[5]) { + points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2); + } + return points; + }, + + + /** + * Draw a path + * @param {Array} path An SVG path in array form + */ + path: function (path) { + return this.createElement('path').attr({ + d: path, + fill: NONE + }); + }, + + /** + * Draw and return an SVG circle + * @param {Number} x The x position + * @param {Number} y The y position + * @param {Number} r The radius + */ + circle: function (x, y, r) { + var attr = isObject(x) ? + x : + { + x: x, + y: y, + r: r + }; + + return this.createElement('circle').attr(attr); + }, + + /** + * Draw and return an arc + * @param {Number} x X position + * @param {Number} y Y position + * @param {Number} r Radius + * @param {Number} innerR Inner radius like used in donut charts + * @param {Number} start Starting angle + * @param {Number} end Ending angle + */ + arc: function (x, y, r, innerR, start, end) { + // arcs are defined as symbols for the ability to set + // attributes in attr and animate + + if (isObject(x)) { + y = x.y; + r = x.r; + innerR = x.innerR; + start = x.start; + end = x.end; + x = x.x; + } + + return this.symbol('arc', x || 0, y || 0, r || 0, { + innerR: innerR || 0, + start: start || 0, + end: end || 0 + }); + }, + + /** + * Draw and return a rectangle + * @param {Number} x Left position + * @param {Number} y Top position + * @param {Number} width + * @param {Number} height + * @param {Number} r Border corner radius + * @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing + */ + rect: function (x, y, width, height, r, strokeWidth) { + if (isObject(x)) { + y = x.y; + width = x.width; + height = x.height; + r = x.r; + x = x.x; + } + var wrapper = this.createElement('rect').attr({ + rx: r, + ry: r, + fill: NONE + }); + + return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0))); + }, + + /** + * Resize the box and re-align all aligned elements + * @param {Object} width + * @param {Object} height + * @param {Boolean} animate + * + */ + setSize: function(width, height, animate) { + var renderer = this, + alignedObjects = renderer.alignedObjects, + i = alignedObjects.length; + + renderer.width = width; + renderer.height = height; + + renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({ + width: width, + height: height + }); + + while (i--) { + alignedObjects[i].align(); + } + }, + + /** + * Create a group + * @param {String} name The group will be given a class name of 'highcharts-{name}'. + * This can be used for styling and scripting. + */ + g: function(name) { + return this.createElement('g').attr( + defined(name) && { 'class': PREFIX + name } + ); + }, + + /** + * Display an image + * @param {String} src + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + image: function(src, x, y, width, height) { + var attribs = { + preserveAspectRatio: NONE + }, + elemWrapper; + + // optional properties + if (arguments.length > 1) { + extend(attribs, { + x: x, + y: y, + width: width, + height: height + }); + } + + elemWrapper = this.createElement('image').attr(attribs); + + // set the href in the xlink namespace + elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink', + 'href', src); + + return elemWrapper; + }, + + /** + * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object. + * + * @param {Object} symbol + * @param {Object} x + * @param {Object} y + * @param {Object} radius + * @param {Object} options + */ + symbol: function(symbol, x, y, radius, options) { + + var obj, + + // get the symbol definition function + symbolFn = this.symbols[symbol], + + // check if there's a path defined for this symbol + path = symbolFn && symbolFn( + x, + y, + radius, + options + ), + + imageRegex = /^url\((.*?)\)$/, + imageSrc; + + if (path) { + + obj = this.path(path); + // expando properties for use in animate and attr + extend(obj, { + symbolName: symbol, + x: x, + y: y, + r: radius + }); + if (options) { + extend(obj, options); + } + + + // image symbols + } else if (imageRegex.test(symbol)) { + + imageSrc = symbol.match(imageRegex)[1]; + + // create the image synchronously, add attribs async + obj = this.image(imageSrc) + .attr({ + x: x, + y: y + }); + + // create a dummy JavaScript image to get the width and height + createElement('img', { + onload: function() { + var img = this, + size = symbolSizes[img.src] || [img.width, img.height]; + obj.attr({ + width: size[0], + height: size[1] + }).translate( + -mathRound(size[0] / 2), + -mathRound(size[1] / 2) + ); + }, + src: imageSrc + }); + + // default circles + } else { + obj = this.circle(x, y, radius); + } + + return obj; + }, + + /** + * An extendable collection of functions for defining symbol paths. + */ + symbols: { + 'square': function (x, y, radius) { + var len = 0.707 * radius; + return [ + M, x-len, y-len, + L, x+len, y-len, + x+len, y+len, + x-len, y+len, + 'Z' + ]; + }, + + 'triangle': function (x, y, radius) { + return [ + M, x, y-1.33 * radius, + L, x+radius, y + 0.67 * radius, + x-radius, y + 0.67 * radius, + 'Z' + ]; + }, + + 'triangle-down': function (x, y, radius) { + return [ + M, x, y + 1.33 * radius, + L, x-radius, y-0.67 * radius, + x+radius, y-0.67 * radius, + 'Z' + ]; + }, + 'diamond': function (x, y, radius) { + return [ + M, x, y-radius, + L, x+radius, y, + x, y+radius, + x-radius, y, + 'Z' + ]; + }, + 'arc': function (x, y, radius, options) { + var start = options.start, + end = options.end - 0.000001, // to prevent cos and sin of start and end from becoming equal on 360 arcs + innerRadius = options.innerR, + cosStart = mathCos(start), + sinStart = mathSin(start), + cosEnd = mathCos(end), + sinEnd = mathSin(end), + longArc = options.end - start < mathPI ? 0 : 1; + + return [ + M, + x + radius * cosStart, + y + radius * sinStart, + 'A', // arcTo + radius, // x radius + radius, // y radius + 0, // slanting + longArc, // long or short arc + 1, // clockwise + x + radius * cosEnd, + y + radius * sinEnd, + L, + x + innerRadius * cosEnd, + y + innerRadius * sinEnd, + 'A', // arcTo + innerRadius, // x radius + innerRadius, // y radius + 0, // slanting + longArc, // long or short arc + 0, // clockwise + x + innerRadius * cosStart, + y + innerRadius * sinStart, + + 'Z' // close + ]; + } + }, + + /** + * Define a clipping rectangle + * @param {String} id + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + clipRect: function (x, y, width, height) { + var wrapper, + id = PREFIX + idCounter++, + + clipPath = this.createElement('clipPath').attr({ + id: id + }).add(this.defs); + + wrapper = this.rect(x, y, width, height, 0).add(clipPath); + wrapper.id = id; + + return wrapper; + }, + + + /** + * Take a color and return it if it's a string, make it a gradient if it's a + * gradient configuration object + * + * @param {Object} color The color or config object + */ + color: function(color, elem, prop) { + var colorObject, + regexRgba = /^rgba/; + if (color && color.linearGradient) { + var renderer = this, + strLinearGradient = 'linearGradient', + linearGradient = color[strLinearGradient], + id = PREFIX + idCounter++, + gradientObject, + stopColor, + stopOpacity; + gradientObject = renderer.createElement(strLinearGradient).attr({ + id: id, + gradientUnits: 'userSpaceOnUse', + x1: linearGradient[0], + y1: linearGradient[1], + x2: linearGradient[2], + y2: linearGradient[3] + }).add(renderer.defs); + + each(color.stops, function(stop) { + if (regexRgba.test(stop[1])) { + colorObject = Color(stop[1]); + stopColor = colorObject.get('rgb'); + stopOpacity = colorObject.get('a'); + } else { + stopColor = stop[1]; + stopOpacity = 1; + } + renderer.createElement('stop').attr({ + offset: stop[0], + 'stop-color': stopColor, + 'stop-opacity': stopOpacity + }).add(gradientObject); + }); + + return 'url('+ this.url +'#'+ id +')'; + + // Webkit and Batik can't show rgba. + } else if (regexRgba.test(color)) { + colorObject = Color(color); + attr(elem, prop +'-opacity', colorObject.get('a')); + + return colorObject.get('rgb'); + + + } else { + return color; + } + + }, + + + /** + * Add text to the SVG object + * @param {String} str + * @param {Number} x Left position + * @param {Number} y Top position + */ + text: function(str, x, y) { + + // declare variables + var defaultChartStyle = defaultOptions.chart.style, + wrapper; + + x = mathRound(pick(x, 0)); + y = mathRound(pick(y, 0)); + + wrapper = this.createElement('text') + .attr({ + x: x, + y: y, + text: str + }) + .css({ + 'font-family': defaultChartStyle.fontFamily, + 'font-size': defaultChartStyle.fontSize + }); + + wrapper.x = x; + wrapper.y = y; + return wrapper; + } +}; // end SVGRenderer + + + + +/* **************************************************************************** + * * + * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE * + * * + * For applications and websites that don't need IE support, like platform * + * targeted mobile apps and web apps, this code can be removed. * + * * + *****************************************************************************/ +var VMLRenderer; +if (!hasSVG) { + +/** + * The VML element wrapper. + */ +var VMLElement = extendClass( SVGElement, { + + /** + * Initialize a new VML element wrapper. It builds the markup as a string + * to minimize DOM traffic. + * @param {Object} renderer + * @param {Object} nodeName + */ + init: function(renderer, nodeName) { + var markup = ['<', nodeName, ' filled="f" stroked="f"'], + style = ['position: ', ABSOLUTE, ';']; + + // divs and shapes need size + if (nodeName == 'shape' || nodeName == DIV) { + style.push('left:0;top:0;width:10px;height:10px;'); + } + if (docMode8) { + style.push('visibility: ', nodeName == DIV ? HIDDEN : VISIBLE); + } + + markup.push(' style="', style.join(''), '"/>'); + + // create element with default attributes and style + if (nodeName) { + markup = nodeName == DIV || nodeName == 'span' || nodeName == 'img' ? + markup.join('') + : renderer.prepVML(markup); + this.element = createElement(markup); + } + + this.renderer = renderer; + }, + + /** + * Add the node to the given parent + * @param {Object} parent + */ + add: function(parent) { + var wrapper = this, + renderer = wrapper.renderer, + element = wrapper.element, + box = renderer.box, + inverted = parent && parent.inverted, + + // get the parent node + parentNode = parent ? + parent.element || parent : + box; + + + // if the parent group is inverted, apply inversion on all children + if (inverted) { // only on groups + renderer.invertChild(element, parentNode); + } + + // issue #140 workaround - related to #61 and #74 + if (docMode8 && parentNode.gVis == HIDDEN) { + css(element, { visibility: HIDDEN }); + } + + // append it + parentNode.appendChild(element); + + // align text after adding to be able to read offset + wrapper.added = true; + if (wrapper.alignOnAdd) { + wrapper.updateTransform(); + } + + return wrapper; + }, + + /** + * Get or set attributes + */ + attr: function(hash, val) { + var key, + value, + i, + element = this.element || {}, + elemStyle = element.style, + nodeName = element.nodeName, + renderer = this.renderer, + symbolName = this.symbolName, + childNodes, + hasSetSymbolSize, + shadows = this.shadows, + skipAttr, + ret = this; + + // single key-value pair + if (isString(hash) && defined(val)) { + key = hash; + hash = {}; + hash[key] = val; + } + + // used as a getter, val is undefined + if (isString(hash)) { + key = hash; + if (key == 'strokeWidth' || key == 'stroke-width') { + ret = this.strokeweight; + } else { + ret = this[key]; + } + + // setter + } else { + for (key in hash) { + value = hash[key]; + skipAttr = false; + + // prepare paths + // symbols + if (symbolName && /^(x|y|r|start|end|width|height|innerR)/.test(key)) { + // if one of the symbol size affecting parameters are changed, + // check all the others only once for each call to an element's + // .attr() method + if (!hasSetSymbolSize) { + + this.symbolAttr(hash); + + hasSetSymbolSize = true; + } + + skipAttr = true; + + } else if (key == 'd') { + value = value || []; + this.d = value.join(' '); // used in getter for animation + + // convert paths + i = value.length; + var convertedPath = []; + while (i--) { + + // Multiply by 10 to allow subpixel precision. + // Substracting half a pixel seems to make the coordinates + // align with SVG, but this hasn't been tested thoroughly + if (isNumber(value[i])) { + convertedPath[i] = mathRound(value[i] * 10) - 5; + } + // close the path + else if (value[i] == 'Z') { + convertedPath[i] = 'x'; + } + else { + convertedPath[i] = value[i]; + } + + } + value = convertedPath.join(' ') || 'x'; + element.path = value; + + // update shadows + if (shadows) { + i = shadows.length; + while (i--) { + shadows[i].path = value; + } + } + skipAttr = true; + + // directly mapped to css + } else if (key == 'zIndex' || key == 'visibility') { + + // issue 61 workaround + if (docMode8 && key == 'visibility' && nodeName == 'DIV') { + element.gVis = value; + childNodes = element.childNodes; + i = childNodes.length; + while (i--) { + css(childNodes[i], { visibility: value }); + } + if (value == VISIBLE) { // issue 74 + value = null; + } + } + + if (value) { + elemStyle[key] = value; + } + + + + skipAttr = true; + + // width and height + } else if (/^(width|height)$/.test(key)) { + + + // clipping rectangle special + if (this.updateClipping) { + this[key] = value; + this.updateClipping(); + + } else { + // normal + elemStyle[key] = value; + } + + skipAttr = true; + + // x and y + } else if (/^(x|y)$/.test(key)) { + + this[key] = value; // used in getter + + if (element.tagName == 'SPAN') { + this.updateTransform(); + + } else { + elemStyle[{ x: 'left', y: 'top' }[key]] = value; + } + + // class name + } else if (key == 'class') { + // IE8 Standards mode has problems retrieving the className + element.className = value; + + // stroke + } else if (key == 'stroke') { + + value = renderer.color(value, element, key); + + key = 'strokecolor'; + + // stroke width + } else if (key == 'stroke-width' || key == 'strokeWidth') { + element.stroked = value ? true : false; + key = 'strokeweight'; + this[key] = value; // used in getter, issue #113 + if (isNumber(value)) { + value += PX; + } + + // dashStyle + } else if (key == 'dashstyle') { + var strokeElem = element.getElementsByTagName('stroke')[0] || + createElement(renderer.prepVML(['']), null, null, element); + strokeElem[key] = value || 'solid'; + this.dashstyle = value; /* because changing stroke-width will change the dash length + and cause an epileptic effect */ + skipAttr = true; + + // fill + } else if (key == 'fill') { + + if (nodeName == 'SPAN') { // text color + elemStyle.color = value; + } else { + element.filled = value != NONE ? true : false; + + value = renderer.color(value, element, key); + + key = 'fillcolor'; + } + + // translation for animation + } else if (key == 'translateX' || key == 'translateY' || key == 'rotation' || key == 'align') { + if (key == 'align') { + key = 'textAlign'; + } + this[key] = value; + this.updateTransform(); + + skipAttr = true; + } + + // text for rotated and non-rotated elements + else if (key == 'text') { + element.innerHTML = value; + skipAttr = true; + } + + + // let the shadow follow the main element + if (shadows && key == 'visibility') { + i = shadows.length; + while (i--) { + shadows[i].style[key] = value; + } + } + + + + if (!skipAttr) { + if (docMode8) { // IE8 setAttribute bug + element[key] = value; + } else { + attr(element, key, value); + } + } + } + } + return ret; + }, + + /** + * Set the element's clipping to a predefined rectangle + * + * @param {String} id The id of the clip rectangle + */ + clip: function(clipRect) { + var wrapper = this, + clipMembers = clipRect.members; + + clipMembers.push(wrapper); + wrapper.destroyClip = function() { + erase(clipMembers, wrapper); + }; + return wrapper.css(clipRect.getCSS(wrapper.inverted)); + }, + + /** + * Set styles for the element + * @param {Object} styles + */ + css: function(styles) { + var wrapper = this, + element = wrapper.element, + textWidth = styles && element.tagName == 'SPAN' && styles.width; + + /*if (textWidth) { + extend(styles, { + display: 'block', + whiteSpace: 'normal' + }); + }*/ + if (textWidth) { + delete styles.width; + wrapper.textWidth = textWidth; + wrapper.updateTransform(); + } + + wrapper.styles = extend(wrapper.styles, styles); + css(wrapper.element, styles); + + + + return wrapper; + }, + + /** + * Extend element.destroy by removing it from the clip members array + */ + destroy: function() { + var wrapper = this; + + if (wrapper.destroyClip) { + wrapper.destroyClip(); + } + + SVGElement.prototype.destroy.apply(wrapper); + }, + + /** + * Remove all child nodes of a group, except the v:group element + */ + empty: function() { + var element = this.element, + childNodes = element.childNodes, + i = childNodes.length, + node; + + while (i--) { + node = childNodes[i]; + node.parentNode.removeChild(node); + } + }, + + /** + * VML override for calculating the bounding box based on offsets + * + * @return {Object} A hash containing values for x, y, width and height + */ + + getBBox: function() { + var element = this.element; + + // faking getBBox in exported SVG in legacy IE + if (element.nodeName == 'text') { + element.style.position = ABSOLUTE; + } + + return { + x: element.offsetLeft, + y: element.offsetTop, + width: element.offsetWidth, + height: element.offsetHeight + }; + + }, + + /** + * Add an event listener. VML override for normalizing event parameters. + * @param {String} eventType + * @param {Function} handler + */ + on: function(eventType, handler) { + // simplest possible event model for internal use + this.element['on'+ eventType] = function() { + var evt = win.event; + evt.target = evt.srcElement; + handler(evt); + }; + return this; + }, + + + /** + * VML override private method to update elements based on internal + * properties based on SVG transform + */ + updateTransform: function(hash) { + // aligning non added elements is expensive + if (!this.added) { + this.alignOnAdd = true; + return; + } + + var wrapper = this, + elem = wrapper.element, + translateX = wrapper.translateX || 0, + translateY = wrapper.translateY || 0, + x = wrapper.x || 0, + y = wrapper.y || 0, + align = wrapper.textAlign || 'left', + alignCorrection = { left: 0, center: 0.5, right: 1 }[align], + nonLeft = align && align != 'left'; + + // apply translate + if (translateX || translateY) { + wrapper.css({ + marginLeft: translateX, + marginTop: translateY + }); + } + + // apply inversion + if (wrapper.inverted) { // wrapper is a group + each(elem.childNodes, function(child) { + wrapper.renderer.invertChild(child, elem); + }); + } + + if (elem.tagName == 'SPAN') { + + var width, height, + rotation = wrapper.rotation, + lineHeight, + radians = 0, + costheta = 1, + sintheta = 0, + quad, + textWidth = pInt(wrapper.textWidth), + xCorr = wrapper.xCorr || 0, + yCorr = wrapper.yCorr || 0, + currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth].join(','); + + if (currentTextTransform != wrapper.cTT) { // do the calculations and DOM access only if properties changed + + if (defined(rotation)) { + radians = rotation * deg2rad; // deg to rad + costheta = mathCos(radians); + sintheta = mathSin(radians); + + // Adjust for alignment and rotation. + // Test case: http://highcharts.com/tests/?file=text-rotation + css(elem, { + filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta, + ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta, + ', sizingMethod=\'auto expand\')'].join('') : NONE + }); + } + + width = elem.offsetWidth; + height = elem.offsetHeight; + + // update textWidth + if (width > textWidth) { + css(elem, { + width: textWidth +PX, + display: 'block', + whiteSpace: 'normal' + }); + width = textWidth; + } + + // correct x and y + lineHeight = mathRound(pInt(elem.style.fontSize || 12) * 1.2); + xCorr = costheta < 0 && -width; + yCorr = sintheta < 0 && -height; + + // correct for lineHeight and corners spilling out after rotation + quad = costheta * sintheta < 0; + xCorr += sintheta * lineHeight * (quad ? 1 - alignCorrection : alignCorrection); + yCorr -= costheta * lineHeight * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1); + + // correct for the length/height of the text + if (nonLeft) { + xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1); + if (rotation) { + yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1); + } + css(elem, { + textAlign: align + }); + } + + // record correction + wrapper.xCorr = xCorr; + wrapper.yCorr = yCorr; + } + + // apply position with correction + css(elem, { + left: x + xCorr, + top: y + yCorr + }); + + // record current text transform + wrapper.cTT = currentTextTransform; + } + }, + + /** + * Apply a drop shadow by copying elements and giving them different strokes + * @param {Boolean} apply + */ + shadow: function(apply) { + var shadows = [], + i, + element = this.element, + renderer = this.renderer, + shadow, + elemStyle = element.style, + markup, + path = element.path; + + // the path is some mysterious string-like object that can be cast to a string + if (''+ element.path === '') { + path = 'x'; + } + + if (apply) { + for (i = 1; i <= 3; i++) { + markup = ['']; + shadow = createElement(renderer.prepVML(markup), + null, { + left: pInt(elemStyle.left) + 1, + top: pInt(elemStyle.top) + 1 + } + ); + + // apply the opacity + markup = ['']; + createElement(renderer.prepVML(markup), null, null, shadow); + + + // insert it + element.parentNode.insertBefore(shadow, element); + + // record it + shadows.push(shadow); + + } + + this.shadows = shadows; + } + return this; + + } +}); + +/** + * The VML renderer + */ +VMLRenderer = function() { + this.init.apply(this, arguments); +}; +VMLRenderer.prototype = merge( SVGRenderer.prototype, { // inherit SVGRenderer + + isIE8: userAgent.indexOf('MSIE 8.0') > -1, + + + /** + * Initialize the VMLRenderer + * @param {Object} container + * @param {Number} width + * @param {Number} height + */ + init: function(container, width, height) { + var renderer = this, + boxWrapper; + + renderer.Element = VMLElement; + renderer.alignedObjects = []; + + boxWrapper = renderer.createElement(DIV); + container.appendChild(boxWrapper.element); + + + // generate the containing box + renderer.box = boxWrapper.element; + renderer.boxWrapper = boxWrapper; + + + renderer.setSize(width, height, false); + + // The only way to make IE6 and IE7 print is to use a global namespace. However, + // with IE8 the only way to make the dynamic shapes visible in screen and print mode + // seems to be to add the xmlns attribute and the behaviour style inline. + if (!doc.namespaces.hcv) { + + doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml'); + + // setup default css + doc.createStyleSheet().cssText = + 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke'+ + '{ behavior:url(#default#VML); display: inline-block; } '; + + } + }, + + /** + * Define a clipping rectangle. In VML it is accomplished by storing the values + * for setting the CSS style to all associated members. + * + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + clipRect: function (x, y, width, height) { + + // create a dummy element + var clipRect = this.createElement(); + + // mimic a rectangle with its style object for automatic updating in attr + return extend(clipRect, { + members: [], + left: x, + top: y, + width: width, + height: height, + getCSS: function(inverted) { + var rect = this,//clipRect.element.style, + top = rect.top, + left = rect.left, + right = left + rect.width, + bottom = top + rect.height, + ret = { + clip: 'rect('+ + mathRound(inverted ? left : top) + 'px,'+ + mathRound(inverted ? bottom : right) + 'px,'+ + mathRound(inverted ? right : bottom) + 'px,'+ + mathRound(inverted ? top : left) +'px)' + }; + + // issue 74 workaround + if (!inverted && docMode8) { + extend(ret, { + width: right +PX, + height: bottom +PX + }); + } + return ret; + }, + + // used in attr and animation to update the clipping of all members + updateClipping: function() { + each(clipRect.members, function(member) { + member.css(clipRect.getCSS(member.inverted)); + }); + } + }); + + }, + + + /** + * Take a color and return it if it's a string, make it a gradient if it's a + * gradient configuration object, and apply opacity. + * + * @param {Object} color The color or config object + */ + color: function(color, elem, prop) { + var colorObject, + regexRgba = /^rgba/, + markup; + + if (color && color.linearGradient) { + + var stopColor, + stopOpacity, + linearGradient = color.linearGradient, + angle, + color1, + opacity1, + color2, + opacity2; + + each(color.stops, function(stop, i) { + if (regexRgba.test(stop[1])) { + colorObject = Color(stop[1]); + stopColor = colorObject.get('rgb'); + stopOpacity = colorObject.get('a'); + } else { + stopColor = stop[1]; + stopOpacity = 1; + } + + if (!i) { // first + color1 = stopColor; + opacity1 = stopOpacity; + } else { + color2 = stopColor; + opacity2 = stopOpacity; + } + }); + + + + // calculate the angle based on the linear vector + angle = 90 - math.atan( + (linearGradient[3] - linearGradient[1]) / // y vector + (linearGradient[2] - linearGradient[0]) // x vector + ) * 180 / mathPI; + + // when colors attribute is used, the meanings of opacity and o:opacity2 + // are reversed. + markup = ['<', prop, ' colors="0% ', color1, ',100% ', color2, '" angle="', angle, + '" opacity="', opacity2, '" o:opacity2="', opacity1, + '" type="gradient" focus="100%" />']; + createElement(this.prepVML(markup), null, null, elem); + + + + // if the color is an rgba color, split it and add a fill node + // to hold the opacity component + } else if (regexRgba.test(color) && elem.tagName != 'IMG') { + + colorObject = Color(color); + + markup = ['<', prop, ' opacity="', colorObject.get('a'), '"/>']; + createElement(this.prepVML(markup), null, null, elem); + + return colorObject.get('rgb'); + + + } else { + return color; + } + + }, + + /** + * Take a VML string and prepare it for either IE8 or IE6/IE7. + * @param {Array} markup A string array of the VML markup to prepare + */ + prepVML: function(markup) { + var vmlStyle = 'display:inline-block;behavior:url(#default#VML);', + isIE8 = this.isIE8; + + markup = markup.join(''); + + if (isIE8) { // add xmlns and style inline + markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />'); + if (markup.indexOf('style="') == -1) { + markup = markup.replace('/>', ' style="'+ vmlStyle +'" />'); + } else { + markup = markup.replace('style="', 'style="'+ vmlStyle); + } + + } else { // add namespace + markup = markup.replace('<', ' 1) { + obj.css({ + left: x, + top: y, + width: width, + height: height + }); + } + return obj; + }, + + /** + * VML uses a shape for rect to overcome bugs and rotation problems + */ + rect: function(x, y, width, height, r, strokeWidth) { + + if (isObject(x)) { + y = x.y; + width = x.width; + height = x.height; + r = x.r; + x = x.x; + } + var wrapper = this.symbol('rect'); + wrapper.r = r; + + return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0))); + }, + + /** + * In the VML renderer, each child of an inverted div (group) is inverted + * @param {Object} element + * @param {Object} parentNode + */ + invertChild: function(element, parentNode) { + var parentStyle = parentNode.style; + + css(element, { + flip: 'x', + left: pInt(parentStyle.width) - 10, + top: pInt(parentStyle.height) - 10, + rotation: -90 + }); + }, + + /** + * Symbol definitions that override the parent SVG renderer's symbols + * + */ + symbols: { + // VML specific arc function + arc: function (x, y, radius, options) { + var start = options.start, + end = options.end, + cosStart = mathCos(start), + sinStart = mathSin(start), + cosEnd = mathCos(end), + sinEnd = mathSin(end), + innerRadius = options.innerR, + circleCorrection = 0.07 / radius, + innerCorrection = innerRadius && 0.1 / innerRadius || 0; + + if (end - start === 0) { // no angle, don't show it. + return ['x']; + + //} else if (end - start == 2 * mathPI) { // full circle + } else if (2 * mathPI - end + start < circleCorrection) { // full circle + // empirical correction found by trying out the limits for different radii + cosEnd = - circleCorrection; + } else if (end - start < innerCorrection) { // issue #186, another mysterious VML arc problem + cosEnd = mathCos(start + innerCorrection); + } + + return [ + 'wa', // clockwise arc to + x - radius, // left + y - radius, // top + x + radius, // right + y + radius, // bottom + x + radius * cosStart, // start x + y + radius * sinStart, // start y + x + radius * cosEnd, // end x + y + radius * sinEnd, // end y + + + 'at', // anti clockwise arc to + x - innerRadius, // left + y - innerRadius, // top + x + innerRadius, // right + y + innerRadius, // bottom + x + innerRadius * cosEnd, // start x + y + innerRadius * sinEnd, // start y + x + innerRadius * cosStart, // end x + y + innerRadius * sinStart, // end y + + 'x', // finish path + 'e' // close + ]; + + }, + // Add circle symbol path. This performs significantly faster than v:oval. + circle: function (x, y, r) { + return [ + 'wa', // clockwisearcto + x - r, // left + y - r, // top + x + r, // right + y + r, // bottom + x + r, // start x + y, // start y + x + r, // end x + y, // end y + //'x', // finish path + 'e' // close + ]; + }, + /** + * Add rectangle symbol path which eases rotation and omits arcsize problems + * compared to the built-in VML roundrect shape + * + * @param {Number} left Left position + * @param {Number} top Top position + * @param {Number} r Border radius + * @param {Object} options Width and height + */ + + rect: function (left, top, r, options) { + if (!defined(options)) { + return []; + } + var width = options.width, + height = options.height, + right = left + width, + bottom = top + height; + + r = mathMin(r, width, height); + + return [ + M, + left + r, top, + + L, + right - r, top, + 'wa', + right - 2 * r, top, + right, top + 2 * r, + right - r, top, + right, top + r, + + L, + right, bottom - r, + 'wa', + right - 2 * r, bottom - 2 * r, + right, bottom, + right, bottom - r, + right - r, bottom, + + L, + left + r, bottom, + 'wa', + left, bottom - 2 * r, + left + 2 * r, bottom, + left + r, bottom, + left, bottom - r, + + L, + left, top + r, + 'wa', + left, top, + left + 2 * r, top + 2 * r, + left, top + r, + left + r, top, + + + 'x', + 'e' + ]; + + } + } +}); +} +/* **************************************************************************** + * * + * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE * + * * + *****************************************************************************/ + +/** + * General renderer + */ +var Renderer = hasSVG ? SVGRenderer : VMLRenderer; + + +/** + * The chart class + * @param {Object} options + * @param {Function} callback Function to run when the chart has loaded + */ +function Chart (options, callback) { + + defaultXAxisOptions = merge(defaultXAxisOptions, defaultOptions.xAxis); + defaultYAxisOptions = merge(defaultYAxisOptions, defaultOptions.yAxis); + defaultOptions.xAxis = defaultOptions.yAxis = null; + + // Handle regular options + options = merge(defaultOptions, options); + + // Define chart variables + var optionsChart = options.chart, + optionsMargin = optionsChart.margin, + margin = isObject(optionsMargin) ? + optionsMargin : + [optionsMargin, optionsMargin, optionsMargin, optionsMargin], + optionsMarginTop = pick(optionsChart.marginTop, margin[0]), + optionsMarginRight = pick(optionsChart.marginRight, margin[1]), + optionsMarginBottom = pick(optionsChart.marginBottom, margin[2]), + optionsMarginLeft = pick(optionsChart.marginLeft, margin[3]), + spacingTop = optionsChart.spacingTop, + spacingRight = optionsChart.spacingRight, + spacingBottom = optionsChart.spacingBottom, + spacingLeft = optionsChart.spacingLeft, + spacingBox, + chartTitleOptions, + chartSubtitleOptions, + plotTop, + marginRight, + marginBottom, + plotLeft, + axisOffset, + renderTo, + renderToClone, + container, + containerId, + containerWidth, + containerHeight, + chartWidth, + chartHeight, + oldChartWidth, + oldChartHeight, + chartBackground, + plotBackground, + plotBGImage, + plotBorder, + chart = this, + chartEvents = optionsChart.events, + runChartClick = chartEvents && !!chartEvents.click, + eventType, + isInsidePlot, // function + tooltip, + mouseIsDown, + loadingDiv, + loadingSpan, + loadingShown, + plotHeight, + plotWidth, + tracker, + trackerGroup, + placeTrackerGroup, + legend, + legendWidth, + legendHeight, + chartPosition,// = getPosition(container), + hasCartesianSeries = optionsChart.showAxes, + isResizing = 0, + axes = [], + maxTicks, // handle the greatest amount of ticks on grouped axes + series = [], + inverted, + renderer, + tooltipTick, + tooltipInterval, + hoverX, + drawChartBox, // function + getMargins, // function + resetMargins, // function + setChartSize, // function + resize, + zoom, // function + zoomOut; // function + + + /** + * Create a new axis object + * @param {Object} chart + * @param {Object} options + */ + function Axis (chart, options) { + + // Define variables + var isXAxis = options.isX, + opposite = options.opposite, // needed in setOptions + horiz = inverted ? !isXAxis : isXAxis, + side = horiz ? + (opposite ? 0 /* top */ : 2 /* bottom */) : + (opposite ? 1 /* right*/ : 3 /* left */ ), + stacks = {}; + + + options = merge( + isXAxis ? defaultXAxisOptions : defaultYAxisOptions, + [defaultTopAxisOptions, defaultRightAxisOptions, + defaultBottomAxisOptions, defaultLeftAxisOptions][side], + options + ); + + var axis = this, + isDatetimeAxis = options.type == 'datetime', + offset = options.offset || 0, + xOrY = isXAxis ? 'x' : 'y', + axisLength, + transA, // translation factor + oldTransA, // used for prerendering + transB = horiz ? plotLeft : marginBottom, // translation addend + translate, // fn + getPlotLinePath, // fn + axisGroup, + gridGroup, + axisLine, + dataMin, + dataMax, + associatedSeries, + userSetMin, + userSetMax, + max = null, + min = null, + oldMin, + oldMax, + minPadding = options.minPadding, + maxPadding = options.maxPadding, + isLinked = defined(options.linkedTo), + ignoreMinPadding, // can be set to true by a column or bar series + ignoreMaxPadding, + usePercentage, + events = options.events, + eventType, + plotLinesAndBands = [], + tickInterval, + minorTickInterval, + magnitude, + tickPositions, // array containing predefined positions + ticks = {}, + minorTicks = {}, + alternateBands = {}, + tickAmount, + labelOffset, + axisTitleMargin,// = options.title.margin, + dateTimeLabelFormat, + categories = options.categories, + labelFormatter = options.labels.formatter || // can be overwritten by dynamic format + function() { + var value = this.value, + ret; + + if (dateTimeLabelFormat) { // datetime axis + ret = dateFormat(dateTimeLabelFormat, value); + + } else if (tickInterval % 1000000 === 0) { // use M abbreviation + ret = (value / 1000000) +'M'; + + } else if (tickInterval % 1000 === 0) { // use k abbreviation + ret = (value / 1000) +'k'; + + } else if (!categories && value >= 1000) { // add thousands separators + ret = numberFormat(value, 0); + + } else { // strings (categories) and small numbers + ret = value; + } + return ret; + }, + + staggerLines = horiz && options.labels.staggerLines, + reversed = options.reversed, + tickmarkOffset = (categories && options.tickmarkPlacement == 'between') ? 0.5 : 0; + + /** + * The Tick class + */ + function Tick(pos, minor) { + var tick = this; + tick.pos = pos; + tick.minor = minor; + tick.isNew = true; + + if (!minor) { + tick.addLabel(); + } + } + Tick.prototype = { + /** + * Write the tick label + */ + addLabel: function() { + var pos = this.pos, + labelOptions = options.labels, + str, + withLabel = !((pos == min && !pick(options.showFirstLabel, 1)) || + (pos == max && !pick(options.showLastLabel, 0))), + width = categories && horiz && categories.length && + !labelOptions.step && !labelOptions.staggerLines && + !labelOptions.rotation && + plotWidth / categories.length || + !horiz && plotWidth / 2, + css, + label = this.label; + + + // get the string + str = labelFormatter.call({ + isFirst: pos == tickPositions[0], + isLast: pos == tickPositions[tickPositions.length - 1], + dateTimeLabelFormat: dateTimeLabelFormat, + value: (categories && categories[pos] ? categories[pos] : pos) + }); + + // prepare CSS + css = width && { width: (width - 2 * (labelOptions.padding || 10)) +PX }; + css = extend(css, labelOptions.style); + + // first call + if (label === UNDEFINED) { + this.label = + defined(str) && withLabel && labelOptions.enabled ? + renderer.text( + str, + 0, + 0 + ) + .attr({ + align: labelOptions.align, + rotation: labelOptions.rotation + }) + // without position absolute, IE export sometimes is wrong + .css(css) + .add(axisGroup): + null; + + // update + } else if (label) { + label.attr({ text: str }) + .css(css); + } + }, + /** + * Get the offset height or width of the label + */ + getLabelSize: function() { + var label = this.label; + return label ? + ((this.labelBBox = label.getBBox()))[horiz ? 'height' : 'width'] : + 0; + }, + /** + * Put everything in place + * + * @param index {Number} + * @param old {Boolean} Use old coordinates to prepare an animation into new position + */ + render: function(index, old) { + var tick = this, + major = !tick.minor, + label = tick.label, + pos = tick.pos, + labelOptions = options.labels, + gridLine = tick.gridLine, + gridLineWidth = major ? options.gridLineWidth : options.minorGridLineWidth, + gridLineColor = major ? options.gridLineColor : options.minorGridLineColor, + dashStyle = major ? + options.gridLineDashStyle : + options.minorGridLineDashStyle, + gridLinePath, + mark = tick.mark, + markPath, + tickLength = major ? options.tickLength : options.minorTickLength, + tickWidth = major ? options.tickWidth : (options.minorTickWidth || 0), + tickColor = major ? options.tickColor : options.minorTickColor, + tickPosition = major ? options.tickPosition : options.minorTickPosition, + step = labelOptions.step, + cHeight = old && oldChartHeight || chartHeight, + attribs, + x, + y; + + // get x and y position for ticks and labels + x = horiz ? + translate(pos + tickmarkOffset, null, null, old) + transB : + plotLeft + offset + (opposite ? (old && oldChartWidth || chartWidth) - marginRight - plotLeft : 0); + + y = horiz ? + cHeight - marginBottom + offset - (opposite ? plotHeight : 0) : + cHeight - translate(pos + tickmarkOffset, null, null, old) - transB; + + // create the grid line + if (gridLineWidth) { + gridLinePath = getPlotLinePath(pos + tickmarkOffset, gridLineWidth, old); + + if (gridLine === UNDEFINED) { + attribs = { + stroke: gridLineColor, + 'stroke-width': gridLineWidth + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + tick.gridLine = gridLine = + gridLineWidth ? + renderer.path(gridLinePath) + .attr(attribs).add(gridGroup) : + null; + } + if (gridLine && gridLinePath) { + gridLine.animate({ + d: gridLinePath + }); + } + } + + // create the tick mark + if (tickWidth) { + + // negate the length + if (tickPosition == 'inside') { + tickLength = -tickLength; + } + if (opposite) { + tickLength = -tickLength; + } + + markPath = renderer.crispLine([ + M, + x, + y, + L, + x + (horiz ? 0 : -tickLength), + y + (horiz ? tickLength : 0) + ], tickWidth); + + if (mark) { // updating + mark.animate({ + d: markPath + }); + } else { // first time + tick.mark = renderer.path( + markPath + ).attr({ + stroke: tickColor, + 'stroke-width': tickWidth + }).add(axisGroup); + } + } + + // the label is created on init - now move it into place + if (label) { + x = x + labelOptions.x - (tickmarkOffset && horiz ? + tickmarkOffset * transA * (reversed ? -1 : 1) : 0); + y = y + labelOptions.y - (tickmarkOffset && !horiz ? + tickmarkOffset * transA * (reversed ? 1 : -1) : 0); + + // vertically centered + if (!defined(labelOptions.y)) { + y += parseInt(label.styles.lineHeight) * 0.9 - label.getBBox().height / 2; + } + + + // correct for staggered labels + if (staggerLines) { + y += (index % staggerLines) * 16; + } + // apply step + if (step) { + // show those indices dividable by step + label[index % step ? 'hide' : 'show'](); + } + + label[tick.isNew ? 'attr' : 'animate']({ + x: x, + y: y + }); + + } + + tick.isNew = false; + }, + /** + * Destructor for the tick prototype + */ + destroy: function() { + var tick = this, + n; + for (n in tick) { + if (tick[n] && tick[n].destroy) { + tick[n].destroy(); + } + } + } + }; + + /** + * The object wrapper for plot lines and plot bands + * @param {Object} options + */ + function PlotLineOrBand(options) { + var plotLine = this; + if (options) { + plotLine.options = options; + plotLine.id = options.id; + } + + //plotLine.render() + return plotLine; + } + + PlotLineOrBand.prototype = { + + /** + * Render the plot line or plot band. If it is already existing, + * move it. + */ + render: function () { + var plotLine = this, + options = plotLine.options, + optionsLabel = options.label, + label = plotLine.label, + width = options.width, + to = options.to, + toPath, // bands only + from = options.from, + dashStyle = options.dashStyle, + svgElem = plotLine.svgElem, + path = [], + addEvent, + eventType, + xs, + ys, + x, + y, + color = options.color, + zIndex = options.zIndex, + events = options.events, + attribs; + + // plot line + if (width) { + path = getPlotLinePath(options.value, width); + attribs = { + stroke: color, + 'stroke-width': width + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + } + + // plot band + else if (defined(from) && defined(to)) { + // keep within plot area + from = mathMax(from, min); + to = mathMin(to, max); + + toPath = getPlotLinePath(to); + path = getPlotLinePath(from); + if (path && toPath) { + path.push( + toPath[4], + toPath[5], + toPath[1], + toPath[2] + ); + } else { // outside the axis area + path = null; + } + attribs = { + fill: color + }; + } else { + return; + } + // zIndex + if (defined(zIndex)) { + attribs.zIndex = zIndex; + } + + // common for lines and bands + if (svgElem) { + if (path) { + svgElem.animate({ + d: path + }, null, svgElem.onGetPath); + } else { + svgElem.hide(); + svgElem.onGetPath = function() { + svgElem.show(); + } + } + } else if (path && path.length) { + plotLine.svgElem = svgElem = renderer.path(path) + .attr(attribs).add(); + + // events + if (events) { + addEvent = function(eventType) { + svgElem.on(eventType, function(e) { + events[eventType].apply(plotLine, [e]); + }); + }; + for (eventType in events) { + addEvent(eventType); + } + } + } + + // the plot band/line label + if (optionsLabel && defined(optionsLabel.text) && path && path.length && plotWidth > 0 && plotHeight > 0) { + // apply defaults + optionsLabel = merge({ + align: horiz && toPath && 'center', + x: horiz ? !toPath && 4 : 10, + verticalAlign : !horiz && toPath && 'middle', + y: horiz ? toPath ? 16 : 10 : toPath ? 6 : -4, + rotation: horiz && !toPath && 90 + }, optionsLabel); + + // add the SVG element + if (!label) { + plotLine.label = label = renderer.text( + optionsLabel.text, + 0, + 0 + ) + .attr({ + align: optionsLabel.textAlign || optionsLabel.align, + rotation: optionsLabel.rotation, + zIndex: zIndex + }) + .css(optionsLabel.style) + .add(); + } + + // get the bounding box and align the label + xs = [path[1], path[4], path[6] || path[1]]; + ys = [path[2], path[5], path[7] || path[2]]; + x = mathMin.apply(math, xs); + y = mathMin.apply(math, ys); + + label.align(optionsLabel, false, { + x: x, + y: y, + width: mathMax.apply(math, xs) - x, + height: mathMax.apply(math, ys) - y + }); + label.show(); + + } else if (label) { // move out of sight + label.hide(); + } + + // chainable + return plotLine; + }, + + /** + * Remove the plot line or band + */ + destroy: function() { + var obj = this, + n; + + for (n in obj) { + if (obj[n] && obj[n].destroy) { + obj[n].destroy(); // destroy SVG wrappers + } + delete obj[n]; + } + // remove it from the lookup + erase(plotLinesAndBands, obj); + } + }; + + + /** + * Get the minimum and maximum for the series of each axis + */ + function getSeriesExtremes() { + var posStack = [], + negStack = [], + run; + + // reset dataMin and dataMax in case we're redrawing + dataMin = dataMax = null; + + // get an overview of what series are associated with this axis + associatedSeries = []; + + each(series, function(serie) { + run = false; + + + // match this axis against the series' given or implicated axis + each(['xAxis', 'yAxis'], function(strAxis) { + if ( + // the series is a cartesian type, and... + serie.isCartesian && + // we're in the right x or y dimension, and... + (strAxis == 'xAxis' && isXAxis || strAxis == 'yAxis' && !isXAxis) && ( + // the axis number is given in the options and matches this axis index, or + (serie.options[strAxis] == options.index) || + // the axis index is not given + (serie.options[strAxis] === UNDEFINED && options.index === 0) + ) + ) { + serie[strAxis] = axis; + associatedSeries.push(serie); + + // the series is visible, run the min/max detection + run = true; + } + }); + // ignore hidden series if opted + if (!serie.visible && optionsChart.ignoreHiddenSeries) { + run = false; + } + + if (run) { + + var stacking, + posPointStack, + negPointStack, + stackKey, + negKey; + + if (!isXAxis) { + stacking = serie.options.stacking; + usePercentage = stacking == 'percent'; + + // create a stack for this particular series type + if (stacking) { + stackKey = serie.type + pick(serie.options.stack, ''); + negKey = '-'+ stackKey; + serie.stackKey = stackKey; // used in translate + + posPointStack = posStack[stackKey] || []; // contains the total values for each x + posStack[stackKey] = posPointStack; + + negPointStack = negStack[negKey] || []; + negStack[negKey] = negPointStack; + } + if (usePercentage) { + dataMin = 0; + dataMax = 99; + } + } + if (serie.isCartesian) { // line, column etc. need axes, pie doesn't + each(serie.data, function(point, i) { + var pointX = point.x, + pointY = point.y, + isNegative = pointY < 0, + pointStack = isNegative ? negPointStack : posPointStack, + key = isNegative ? negKey : stackKey, + totalPos, + pointLow; + + // initial values + if (dataMin === null) { + + // start out with the first point + dataMin = dataMax = point[xOrY]; + } + + // x axis + if (isXAxis) { + if (pointX > dataMax) { + dataMax = pointX; + } else if (pointX < dataMin) { + dataMin = pointX; + } + } + + // y axis + else if (defined(pointY)) { + if (stacking) { + pointStack[pointX] = + defined(pointStack[pointX]) ? + pointStack[pointX] + pointY : pointY; + } + totalPos = pointStack ? pointStack[pointX] : pointY; + pointLow = pick(point.low, totalPos); + if (!usePercentage) { + if (totalPos > dataMax) { + dataMax = totalPos; + } else if (pointLow < dataMin) { + dataMin = pointLow; + } + } + if (stacking) { + // add the series + if (!stacks[key]) { + stacks[key] = {}; + } + stacks[key][pointX] = { + total: totalPos, + cum: totalPos + }; + } + } + }); + + + // For column, areas and bars, set the minimum automatically to zero + // and prevent that minPadding is added in setScale + if (/(area|column|bar)/.test(serie.type) && !isXAxis) { + if (dataMin >= 0) { + dataMin = 0; + ignoreMinPadding = true; + } else if (dataMax < 0) { + dataMax = 0; + ignoreMaxPadding = true; + } + } + } + } + }); + + } + + /** + * Translate from axis value to pixel position on the chart, or back + * + */ + translate = function(val, backwards, cvsCoord, old) { + var sign = 1, + cvsOffset = 0, + localA = old ? oldTransA : transA, + localMin = old ? oldMin : min, + returnValue; + + if (!localA) { + localA = transA; + } + + if (cvsCoord) { + sign *= -1; // canvas coordinates inverts the value + cvsOffset = axisLength; + } + if (reversed) { // reversed axis + sign *= -1; + cvsOffset -= sign * axisLength; + } + + if (backwards) { // reverse translation + if (reversed) { + val = axisLength - val; + } + returnValue = val / localA + localMin; // from chart pixel to value + + } else { // normal translation + returnValue = sign * (val - localMin) * localA + cvsOffset; // from value to chart pixel + } + + return returnValue; + }; + + /** + * Create the path for a plot line that goes from the given value on + * this axis, across the plot to the opposite side + * @param {Number} value + * @param {Number} lineWidth Used for calculation crisp line + * @param {Number] old Use old coordinates (for resizing and rescaling) + */ + getPlotLinePath = function(value, lineWidth, old) { + var x1, + y1, + x2, + y2, + translatedValue = translate(value, null, null, old), + cHeight = old && oldChartHeight || chartHeight, + cWidth = old && oldChartWidth || chartWidth, + skip; + + x1 = x2 = mathRound(translatedValue + transB); + y1 = y2 = mathRound(cHeight - translatedValue - transB); + + if (isNaN(translatedValue)) { // no min or max + skip = true; + + } else if (horiz) { + y1 = plotTop; + y2 = cHeight - marginBottom; + if (x1 < plotLeft || x1 > plotLeft + plotWidth) { + skip = true; + } + } else { + x1 = plotLeft; + x2 = cWidth - marginRight; + if (y1 < plotTop || y1 > plotTop + plotHeight) { + skip = true; + } + } + return skip ? + null : + renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 0); + }; + + /** + * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 + * @param {Number} interval + */ + function normalizeTickInterval(interval, multiples) { + var normalized; + + // round to a tenfold of 1, 2, 2.5 or 5 + magnitude = multiples ? 1 : math.pow(10, mathFloor(math.log(interval) / math.LN10)); + normalized = interval / magnitude; + + // multiples for a linear scale + if (!multiples) { + multiples = [1, 2, 2.5, 5, 10]; + //multiples = [1, 2, 2.5, 4, 5, 7.5, 10]; + + // the allowDecimals option + if (options.allowDecimals === false) { + if (magnitude == 1) { + multiples = [1, 2, 5, 10]; + } else if (magnitude <= 0.1) { + multiples = [1 / magnitude]; + } + } + } + + // normalize the interval to the nearest multiple + for (var i = 0; i < multiples.length; i++) { + interval = multiples[i]; + if (normalized <= (multiples[i] + (multiples[i+1] || multiples[i])) / 2) { + break; + } + } + + // multiply back to the correct magnitude + interval *= magnitude; + + return interval; + } + + /** + * Set the tick positions to a time unit that makes sense, for example + * on the first of each month or on every Monday. + */ + function setDateTimeTickPositions() { + tickPositions = []; + var i, + useUTC = defaultOptions.global.useUTC, + oneSecond = 1000 / timeFactor, + oneMinute = 60000 / timeFactor, + oneHour = 3600000 / timeFactor, + oneDay = 24 * 3600000 / timeFactor, + oneWeek = 7 * 24 * 3600000 / timeFactor, + oneMonth = 30 * 24 * 3600000 / timeFactor, + oneYear = 31556952000 / timeFactor, + + units = [[ + 'second', // unit name + oneSecond, // fixed incremental unit + [1, 2, 5, 10, 15, 30] // allowed multiples + ], [ + 'minute', // unit name + oneMinute, // fixed incremental unit + [1, 2, 5, 10, 15, 30] // allowed multiples + ], [ + 'hour', // unit name + oneHour, // fixed incremental unit + [1, 2, 3, 4, 6, 8, 12] // allowed multiples + ], [ + 'day', // unit name + oneDay, // fixed incremental unit + [1, 2] // allowed multiples + ], [ + 'week', // unit name + oneWeek, // fixed incremental unit + [1, 2] // allowed multiples + ], [ + 'month', + oneMonth, + [1, 2, 3, 4, 6] + ], [ + 'year', + oneYear, + null + ]], + + unit = units[6], // default unit is years + interval = unit[1], + multiples = unit[2]; + + // loop through the units to find the one that best fits the tickInterval + for (i = 0; i < units.length; i++) { + unit = units[i]; + interval = unit[1]; + multiples = unit[2]; + + + if (units[i+1]) { + // lessThan is in the middle between the highest multiple and the next unit. + var lessThan = (interval * multiples[multiples.length - 1] + + units[i + 1][1]) / 2; + + // break and keep the current unit + if (tickInterval <= lessThan) { + break; + } + } + } + + // prevent 2.5 years intervals, though 25, 250 etc. are allowed + if (interval == oneYear && tickInterval < 5 * interval) { + multiples = [1, 2, 5]; + } + + // get the minimum value by flooring the date + var multitude = normalizeTickInterval(tickInterval / interval, multiples), + minYear, // used in months and years as a basis for Date.UTC() + minDate = new Date(min * timeFactor); + + minDate.setMilliseconds(0); + + if (interval >= oneSecond) { // second + minDate.setSeconds(interval >= oneMinute ? 0 : + multitude * mathFloor(minDate.getSeconds() / multitude)); + } + + if (interval >= oneMinute) { // minute + minDate[setMinutes](interval >= oneHour ? 0 : + multitude * mathFloor(minDate[getMinutes]() / multitude)); + } + + if (interval >= oneHour) { // hour + minDate[setHours](interval >= oneDay ? 0 : + multitude * mathFloor(minDate[getHours]() / multitude)); + } + + if (interval >= oneDay) { // day + minDate[setDate](interval >= oneMonth ? 1 : + multitude * mathFloor(minDate[getDate]() / multitude)); + } + + if (interval >= oneMonth) { // month + minDate[setMonth](interval >= oneYear ? 0 : + multitude * mathFloor(minDate[getMonth]() / multitude)); + minYear = minDate[getFullYear](); + } + + if (interval >= oneYear) { // year + minYear -= minYear % multitude; + minDate[setFullYear](minYear); + } + + // week is a special case that runs outside the hierarchy + if (interval == oneWeek) { + // get start of current week, independent of multitude + minDate[setDate](minDate[getDate]() - minDate[getDay]() + + options.startOfWeek); + } + + + // get tick positions + i = 1; // prevent crash just in case + minYear = minDate[getFullYear](); + var time = minDate.getTime() / timeFactor, + minMonth = minDate[getMonth](), + minDateDate = minDate[getDate](); + + // iterate and add tick positions at appropriate values + while (time < max && i < plotWidth) { + tickPositions.push(time); + + // if the interval is years, use Date.UTC to increase years + if (interval == oneYear) { + time = makeTime(minYear + i * multitude, 0) / timeFactor; + + // if the interval is months, use Date.UTC to increase months + } else if (interval == oneMonth) { + time = makeTime(minYear, minMonth + i * multitude) / timeFactor; + + // if we're using global time, the interval is not fixed as it jumps + // one hour at the DST crossover + } else if (!useUTC && (interval == oneDay || interval == oneWeek)) { + time = makeTime(minYear, minMonth, minDateDate + + i * multitude * (interval == oneDay ? 1 : 7)); + + // else, the interval is fixed and we use simple addition + } else { + time += interval * multitude; + } + + i++; + } + // push the last time + tickPositions.push(time); + + + // dynamic label formatter + dateTimeLabelFormat = options.dateTimeLabelFormats[unit[0]]; + } + + /** + * Fix JS round off float errors + * @param {Number} num + */ + function correctFloat(num) { + var invMag, ret = num; + if (defined(magnitude)) { + invMag = (magnitude < 1 ? mathRound(1 / magnitude) : 1) * 10; + ret = mathRound(num * invMag) / invMag; + } + return ret; + } + + /** + * Set the tick positions of a linear axis to round values like whole tens or every five. + */ + function setLinearTickPositions() { + + var i, + roundedMin = mathFloor(min / tickInterval) * tickInterval, + roundedMax = mathCeil(max / tickInterval) * tickInterval; + + tickPositions = []; + + // populate the intermediate values + i = correctFloat(roundedMin); + while (i <= roundedMax) { + tickPositions.push(i); + i = correctFloat(i + tickInterval); + } + + } + + /** + * Set the tick positions to round values and optionally extend the extremes + * to the nearest tick + */ + function setTickPositions(secondPass) { + var length, + catPad, + linkedParent, + linkedParentExtremes, + tickIntervalOption = options.tickInterval, + tickPixelIntervalOption = options.tickPixelInterval, + maxZoom = options.maxZoom || ( + isXAxis ? + mathMin(chart.smallestInterval * 5, dataMax - dataMin) : + null + ), + zoomOffset; + + + axisLength = horiz ? plotWidth : plotHeight; + + // linked axis gets the extremes from the parent axis + if (isLinked) { + linkedParent = chart[isXAxis ? 'xAxis' : 'yAxis'][options.linkedTo]; + linkedParentExtremes = linkedParent.getExtremes(); + min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin); + max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax); + } + + // initial min and max from the extreme data values + else { + min = pick(userSetMin, options.min, dataMin); + max = pick(userSetMax, options.max, dataMax); + } + + // maxZoom exceeded, just center the selection + if (max - min < maxZoom) { + zoomOffset = (maxZoom - max + min) / 2; + // if min and max options have been set, don't go beyond it + min = mathMax(min - zoomOffset, pick(options.min, min - zoomOffset), dataMin); + max = mathMin(min + maxZoom, pick(options.max, min + maxZoom), dataMax); + } + + // pad the values to get clear of the chart's edges + if (!categories && !usePercentage && !isLinked && defined(min) && defined(max)) { + length = (max - min) || 1; + if (!defined(options.min) && !defined(userSetMin) && minPadding && (dataMin < 0 || !ignoreMinPadding)) { + min -= length * minPadding; + } + if (!defined(options.max) && !defined(userSetMax) && maxPadding && (dataMax > 0 || !ignoreMaxPadding)) { + max += length * maxPadding; + } + } + + // get tickInterval + if (min == max) { + tickInterval = 1; + } else if (isLinked && !tickIntervalOption && + tickPixelIntervalOption == linkedParent.options.tickPixelInterval) { + tickInterval = linkedParent.tickInterval; + } else { + tickInterval = pick( + tickIntervalOption, + categories ? // for categoried axis, 1 is default, for linear axis use tickPix + 1 : + (max - min) * tickPixelIntervalOption / axisLength + ); + } + + if (!isDatetimeAxis && !defined(options.tickInterval)) { // linear + tickInterval = normalizeTickInterval(tickInterval); + } + axis.tickInterval = tickInterval; // record for linked axis + + // get minorTickInterval + minorTickInterval = options.minorTickInterval === 'auto' && tickInterval ? + tickInterval / 5 : options.minorTickInterval; + + // find the tick positions + if (isDatetimeAxis) { + setDateTimeTickPositions(); + } else { + setLinearTickPositions(); + } + + if (!isLinked) { + // pad categorised axis to nearest half unit + if (categories || (isXAxis && chart.hasColumn)) { + catPad = (categories ? 1 : tickInterval) * 0.5; + if (categories || !defined(pick(options.min, userSetMin))) { + min -= catPad; + } + if (categories || !defined(pick(options.max, userSetMax))) { + max += catPad; + } + } + + // reset min/max or remove extremes based on start/end on tick + var roundedMin = tickPositions[0], + roundedMax = tickPositions[tickPositions.length - 1]; + + if (options.startOnTick) { + min = roundedMin; + } else if (min > roundedMin) { + tickPositions.shift(); + } + + if (options.endOnTick) { + max = roundedMax; + } else if (max < roundedMax) { + tickPositions.pop(); + } + + // record the greatest number of ticks for multi axis + if (!maxTicks) { // first call, or maxTicks have been reset after a zoom operation + maxTicks = { + x: 0, + y: 0 + }; + } + + if (!isDatetimeAxis && tickPositions.length > maxTicks[xOrY]) { + maxTicks[xOrY] = tickPositions.length; + } + } + + + } + + /** + * When using multiple axes, adjust the number of ticks to match the highest + * number of ticks in that group + */ + function adjustTickAmount() { + + if (maxTicks && !isDatetimeAxis && !categories && !isLinked) { // only apply to linear scale + var oldTickAmount = tickAmount, + calculatedTickAmount = tickPositions.length; + + // set the axis-level tickAmount to use below + tickAmount = maxTicks[xOrY]; + + if (calculatedTickAmount < tickAmount) { + while (tickPositions.length < tickAmount) { + tickPositions.push( correctFloat( + tickPositions[tickPositions.length - 1] + tickInterval + )); + } + transA *= (calculatedTickAmount - 1) / (tickAmount - 1); + max = tickPositions[tickPositions.length - 1]; + + } + if (defined(oldTickAmount) && tickAmount != oldTickAmount) { + axis.isDirty = true; + } + } + + } + + /** + * Set the scale based on data min and max, user set min and max or options + * + */ + function setScale() { + var type, + i; + + oldMin = min; + oldMax = max; + + // get data extremes if needed + getSeriesExtremes(); + + // get fixed positions based on tickInterval + setTickPositions(); + + // the translation factor used in translate function + oldTransA = transA; + transA = axisLength / ((max - min) || 1); + + // reset stacks + if (!isXAxis) { + for (type in stacks) { + for (i in stacks[type]) { + stacks[type][i].cum = stacks[type][i].total; + } + } + } + + // mark as dirty if it is not already set to dirty and extremes have changed + if (!axis.isDirty) { + axis.isDirty = (min != oldMin || max != oldMax); + } + + } + + /** + * Set the extremes and optionally redraw + * @param {Number} newMin + * @param {Number} newMax + * @param {Boolean} redraw + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + * + */ + function setExtremes(newMin, newMax, redraw, animation) { + + redraw = pick(redraw, true); // defaults to true + + fireEvent(axis, 'setExtremes', { // fire an event to enable syncing of multiple charts + min: newMin, + max: newMax + }, function() { // the default event handler + + userSetMin = newMin; + userSetMax = newMax; + + + // redraw + if (redraw) { + chart.redraw(animation); + } + }); + + } + + /** + * Get the actual axis extremes + */ + function getExtremes() { + return { + min: min, + max: max, + dataMin: dataMin, + dataMax: dataMax + }; + } + + /** + * Get the zero plane either based on zero or on the min or max value. + * Used in bar and area plots + */ + function getThreshold(threshold) { + if (min > threshold) { + threshold = min; + } else if (max < threshold) { + threshold = max; + } + + return translate(threshold, 0, 1); + } + + /** + * Add a plot band or plot line after render time + * + * @param options {Object} The plotBand or plotLine configuration object + */ + function addPlotBandOrLine(options) { + var obj = new PlotLineOrBand(options).render(); + plotLinesAndBands.push(obj); + return obj; + } + + /** + * Render the tick labels to a preliminary position to get their sizes + */ + function getOffset() { + + var hasData = associatedSeries.length && defined(min) && defined(max), + titleOffset = 0, + titleMargin = 0, + axisTitleOptions = options.title, + labelOptions = options.labels, + directionFactor = [-1, 1, 1, -1][side]; + + if (!axisGroup) { + axisGroup = renderer.g('axis') + .attr({ zIndex: 7 }) + .add(); + gridGroup = renderer.g('grid') + .attr({ zIndex: 1 }) + .add(); + } + + labelOffset = 0; // reset + + if (hasData || isLinked) { + each(tickPositions, function(pos) { + if (!ticks[pos]) { + ticks[pos] = new Tick(pos); + } else { + ticks[pos].addLabel(); // update labels depending on tick interval + } + + // left side must be align: right and right side must have align: left for labels + if (side === 0 || side == 2 || { 1: 'left', 3: 'right' }[side] == labelOptions.align) { + + // get the highest offset + labelOffset = mathMax( + ticks[pos].getLabelSize(), + labelOffset + ); + } + + }); + + if (staggerLines) { + labelOffset += (staggerLines - 1) * 16; + } + + } else { // doesn't have data + for (var n in ticks) { + ticks[n].destroy(); + delete ticks[n]; + } + } + + if (axisTitleOptions && axisTitleOptions.text) { + if (!axis.axisTitle) { + axis.axisTitle = renderer.text( + axisTitleOptions.text, + 0, + 0 + ) + .attr({ + zIndex: 7, + rotation: axisTitleOptions.rotation || 0, + align: + axisTitleOptions.textAlign || + { low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align] + }) + .css(axisTitleOptions.style) + .add(); + } + + titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width']; + titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10); + + } + + // handle automatic or user set offset + offset = directionFactor * (options.offset || axisOffset[side]); + + axisTitleMargin = + labelOffset + + (side != 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x']) + + titleMargin; + + axisOffset[side] = mathMax( + axisOffset[side], + axisTitleMargin + titleOffset + directionFactor * offset + ); + + } + + /** + * Render the axis + */ + function render() { + var axisTitleOptions = options.title, + alternateGridColor = options.alternateGridColor, + lineWidth = options.lineWidth, + lineLeft, + lineTop, + linePath, + hasRendered = chart.hasRendered, + slideInTicks = hasRendered && defined(oldMin) && !isNaN(oldMin), + hasData = associatedSeries.length && defined(min) && defined(max); + + // update metrics + axisLength = horiz ? plotWidth : plotHeight; + transA = axisLength / ((max - min) || 1); + transB = horiz ? plotLeft : marginBottom; // translation addend + + // If the series has data draw the ticks. Else only the line and title + if (hasData || isLinked) { + + // minor ticks + if (minorTickInterval && !categories) { + var pos = min + (tickPositions[0] - min) % minorTickInterval; + for (pos; pos <= max; pos += minorTickInterval) { + if (!minorTicks[pos]) { + minorTicks[pos] = new Tick(pos, true); + } + + // render new ticks in old position + if (slideInTicks && minorTicks[pos].isNew) { + minorTicks[pos].render(null, true); + } + + + minorTicks[pos].isActive = true; + minorTicks[pos].render(); + } + } + + // major ticks + each(tickPositions, function(pos, i) { + // linked axes need an extra check to find out if + if (!isLinked || (pos >= min && pos <= max)) { + + // render new ticks in old position + if (slideInTicks && ticks[pos].isNew) { + ticks[pos].render(i, true); + } + + ticks[pos].isActive = true; + ticks[pos].render(i); + } + }); + + // alternate grid color + if (alternateGridColor) { + each(tickPositions, function(pos, i) { + if (i % 2 === 0 && pos < max) { + /*plotLinesAndBands.push(new PlotLineOrBand({ + from: pos, + to: tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] : max, + color: alternateGridColor + }));*/ + + if (!alternateBands[pos]) { + alternateBands[pos] = new PlotLineOrBand(); + } + alternateBands[pos].options = { + from: pos, + to: tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] : max, + color: alternateGridColor + }; + alternateBands[pos].render(); + alternateBands[pos].isActive = true; + } + }); + } + + // custom plot bands (behind grid lines) + /*if (!hasRendered) { // only first time + each(options.plotBands || [], function(plotBandOptions) { + plotLinesAndBands.push(new PlotLineOrBand( + extend({ zIndex: 1 }, plotBandOptions) + ).render()); + }); + }*/ + + + + + // custom plot lines and bands + if (!hasRendered) { // only first time + each((options.plotLines || []).concat(options.plotBands || []), function(plotLineOptions) { + plotLinesAndBands.push(new PlotLineOrBand(plotLineOptions).render()); + }); + } + + + + } // end if hasData + + // remove inactive ticks + each([ticks, minorTicks, alternateBands], function(coll) { + for (var pos in coll) { + if (!coll[pos].isActive) { + coll[pos].destroy(); + delete coll[pos]; + } else { + coll[pos].isActive = false; // reset + } + } + }); + + + + + // Static items. As the axis group is cleared on subsequent calls + // to render, these items are added outside the group. + // axis line + if (lineWidth) { + lineLeft = plotLeft + (opposite ? plotWidth : 0) + offset; + lineTop = chartHeight - marginBottom - (opposite ? plotHeight : 0) + offset; + + linePath = renderer.crispLine([ + M, + horiz ? + plotLeft: + lineLeft, + horiz ? + lineTop: + plotTop, + L, + horiz ? + chartWidth - marginRight : + lineLeft, + horiz ? + lineTop: + chartHeight - marginBottom + ], lineWidth); + if (!axisLine) { + axisLine = renderer.path(linePath) + .attr({ + stroke: options.lineColor, + 'stroke-width': lineWidth, + zIndex: 7 + }) + .add(); + } else { + axisLine.animate({ d: linePath }); + } + + } + + if (axis.axisTitle) { + // compute anchor points for each of the title align options + var margin = horiz ? plotLeft : plotTop, + fontSize = pInt(axisTitleOptions.style.fontSize || 12), + // the position in the length direction of the axis + alongAxis = { + low: margin + (horiz ? 0 : axisLength), + middle: margin + axisLength / 2, + high: margin + (horiz ? axisLength : 0) + }[axisTitleOptions.align], + + // the position in the perpendicular direction of the axis + offAxis = (horiz ? plotTop + plotHeight : plotLeft) + + (horiz ? 1 : -1) * // horizontal axis reverses the margin + (opposite ? -1 : 1) * // so does opposite axes + axisTitleMargin + + //(isIE ? fontSize / 3 : 0)+ // preliminary fix for vml's centerline + (side == 2 ? fontSize : 0); + + axis.axisTitle[hasRendered ? 'animate' : 'attr']({ + x: horiz ? + alongAxis: + offAxis + (opposite ? plotWidth : 0) + offset + + (axisTitleOptions.x || 0), // x + y: horiz ? + offAxis - (opposite ? plotHeight : 0) + offset: + alongAxis + (axisTitleOptions.y || 0) // y + }); + + } + + axis.isDirty = false; + } + + /** + * Remove a plot band or plot line from the chart by id + * @param {Object} id + */ + function removePlotBandOrLine(id) { + var i = plotLinesAndBands.length; + while (i--) { + if (plotLinesAndBands[i].id == id) { + plotLinesAndBands[i].destroy(); + } + } + } + + /** + * Redraw the axis to reflect changes in the data or axis extremes + */ + function redraw() { + + // hide tooltip and hover states + if (tracker.resetTracker) { + tracker.resetTracker(); + } + + // render the axis + render(); + + // move plot lines and bands + each(plotLinesAndBands, function(plotLine) { + plotLine.render(); + }); + + // mark associated series as dirty and ready for redraw + each(associatedSeries, function(series) { + series.isDirty = true; + }); + + } + + /** + * Set new axis categories and optionally redraw + * @param {Array} newCategories + * @param {Boolean} doRedraw + */ + function setCategories(newCategories, doRedraw) { + // set the categories + axis.categories = categories = newCategories; + + // force reindexing tooltips + each(associatedSeries, function(series) { + series.translate(); + series.setTooltipPoints(true); + }); + + + // optionally redraw + axis.isDirty = true; + + if (pick(doRedraw, true)) { + chart.redraw(); + } + } + + + + // Run Axis + + // inverted charts have reversed xAxes as default + if (inverted && isXAxis && reversed === UNDEFINED) { + reversed = true; + } + + + // expose some variables + extend(axis, { + addPlotBand: addPlotBandOrLine, + addPlotLine: addPlotBandOrLine, + adjustTickAmount: adjustTickAmount, + categories: categories, + getExtremes: getExtremes, + getPlotLinePath: getPlotLinePath, + getThreshold: getThreshold, + isXAxis: isXAxis, + options: options, + plotLinesAndBands: plotLinesAndBands, + getOffset: getOffset, + render: render, + setCategories: setCategories, + setExtremes: setExtremes, + setScale: setScale, + setTickPositions: setTickPositions, + translate: translate, + redraw: redraw, + removePlotBand: removePlotBandOrLine, + removePlotLine: removePlotBandOrLine, + reversed: reversed, + stacks: stacks + }); + + // register event listeners + for (eventType in events) { + addEvent(axis, eventType, events[eventType]); + } + + // set min and max + setScale(); + + } // end Axis + + + /** + * The toolbar object + * + * @param {Object} chart + */ + function Toolbar(chart) { + var buttons = {}; + + function add(id, text, title, fn) { + if (!buttons[id]) { + var button = renderer.text( + text, + 0, + 0 + ) + .css(options.toolbar.itemStyle) + .align({ + align: 'right', + x: - marginRight - 20, + y: plotTop + 30 + }) + .on('click', fn) + /*.on('touchstart', function(e) { + e.stopPropagation(); // don't fire the container event + fn(); + })*/ + .attr({ + align: 'right', + zIndex: 20 + }) + .add(); + buttons[id] = button; + } + } + function remove(id) { + discardElement(buttons[id].element); + buttons[id] = null; + } + + // public + return { + add: add, + remove: remove + }; + } + + /** + * The tooltip object + * @param {Object} options Tooltip options + */ + function Tooltip (options) { + var currentSeries, + borderWidth = options.borderWidth, + crosshairsOptions = options.crosshairs, + crosshairs = [], + style = options.style, + shared = options.shared, + padding = pInt(style.padding), + boxOffLeft = borderWidth + padding, // off left/top position as IE can't + //properly handle negative positioned shapes + tooltipIsHidden = true, + boxWidth, + boxHeight, + currentX = 0, + currentY = 0; + + // remove padding CSS and apply padding on box instead + style.padding = 0; + + // create the elements + var group = renderer.g('tooltip') + .attr({ zIndex: 8 }) + .add(), + + box = renderer.rect(boxOffLeft, boxOffLeft, 0, 0, options.borderRadius, borderWidth) + .attr({ + fill: options.backgroundColor, + 'stroke-width': borderWidth + }) + .add(group) + .shadow(options.shadow), + label = renderer.text('', padding + boxOffLeft, pInt(style.fontSize) + padding + boxOffLeft) + .attr({ zIndex: 1 }) + .css(style) + .add(group); + + group.hide(); + + /** + * In case no user defined formatter is given, this will be used + */ + function defaultFormatter() { + var pThis = this, + items = pThis.points || splat(pThis), + xAxis = items[0].series.xAxis, + x = pThis.x, + isDateTime = xAxis && xAxis.options.type == 'datetime', + useHeader = isString(x) || isDateTime, + series, + s; + + // build the header + s = useHeader ? + ['', + (isDateTime ? dateFormat('%A, %b %e, %Y', x) : x), + '
'] : []; + + // build the values + each(items, function(item) { + s.push(item.point.tooltipFormatter(useHeader)); + }); + return s.join(''); + } + + /** + * Provide a soft movement for the tooltip + * + * @param {Number} finalX + * @param {Number} finalY + */ + function move(finalX, finalY) { + + currentX = tooltipIsHidden ? finalX : (2 * currentX + finalX) / 3; + currentY = tooltipIsHidden ? finalY : (currentY + finalY) / 2; + + group.translate(currentX, currentY); + + + // run on next tick of the mouse tracker + if (mathAbs(finalX - currentX) > 1 || mathAbs(finalY - currentY) > 1) { + tooltipTick = function() { + move(finalX, finalY); + }; + } else { + tooltipTick = null; + } + } + + /** + * Hide the tooltip + */ + function hide() { + if (!tooltipIsHidden) { + var hoverPoints = chart.hoverPoints; + + group.hide(); + + each(crosshairs, function(crosshair) { + if (crosshair) { + crosshair.hide(); + } + }); + + // hide previous hoverPoints and set new + if (hoverPoints) { + each (hoverPoints, function(point) { + point.setState(); + }); + } + chart.hoverPoints = null; + + + tooltipIsHidden = true; + } + + } + + /** + * Refresh the tooltip's text and position. + * @param {Object} point + * + */ + function refresh(point) { + var x, + y, + boxX, + boxY, + show, + bBox, + plotX, + plotY = 0, + textConfig = {}, + text, + pointConfig = [], + tooltipPos = point.tooltipPos, + formatter = options.formatter || defaultFormatter, + hoverPoints = chart.hoverPoints, + getConfig = function(point) { + return { + series: point.series, + point: point, + x: point.category, + y: point.y, + percentage: point.percentage, + total: point.total || point.stackTotal + }; + }; + + // shared tooltip, array is sent over + if (shared) { + + // hide previous hoverPoints and set new + if (hoverPoints) { + each (hoverPoints, function(point) { + point.setState(); + }); + } + chart.hoverPoints = point; + + each(point, function(item, i) { + /*var series = item.series, + hoverPoint = series.hoverPoint; + if (hoverPoint) { + hoverPoint.setState(); + } + series.hoverPoint = item;*/ + item.setState(HOVER_STATE); + plotY += item.plotY; // for average + + pointConfig.push(getConfig(item)); + }); + + plotX = point[0].plotX; + plotY = mathRound(plotY) / point.length; // mathRound because Opera 10 has problems here + + textConfig = { + x: point[0].category + }; + textConfig.points = pointConfig; + point = point[0]; + + // single point tooltip + } else { + textConfig = getConfig(point); + } + text = formatter.call(textConfig); + + // register the current series + currentSeries = point.series; + + // get the reference point coordinates (pie charts use tooltipPos) + plotX = shared ? plotX : point.plotX; + plotY = shared ? plotY : point.plotY; + x = mathRound(tooltipPos ? tooltipPos[0] : (inverted ? plotWidth - plotY : plotX)); + y = mathRound(tooltipPos ? tooltipPos[1] : (inverted ? plotHeight - plotX : plotY)); + + + // hide tooltip if the point falls outside the plot + show = shared || !point.series.isCartesian || isInsidePlot(x, y); + + // update the inner HTML + if (text === false || !show) { + hide(); + } else { + + // show it + if (tooltipIsHidden) { + group.show(); + tooltipIsHidden = false; + } + + // update text + label.attr({ + text: text + }); + + // get the bounding box + bBox = label.getBBox(); + boxWidth = bBox.width + 2 * padding; + boxHeight = bBox.height + 2 * padding; + + // set the size of the box + box.attr({ + width: boxWidth, + height: boxHeight, + stroke: options.borderColor || point.color || currentSeries.color || '#606060' + }); + + // keep the box within the chart area + boxX = x - boxWidth + plotLeft - 25; + boxY = y - boxHeight + plotTop + 10; + + // it is too far to the left, adjust it + if (boxX < 7) { + boxX = 7; + boxY -= 30; + } + + + if (boxY < 5) { + boxY = 5; // above + } else if (boxY + boxHeight > chartHeight) { + boxY = chartHeight - boxHeight - 5; // below + } + + // do the move + move(mathRound(boxX - boxOffLeft), mathRound(boxY - boxOffLeft)); + + + } + + + // crosshairs + if (crosshairsOptions) { + crosshairsOptions = splat(crosshairsOptions); // [x, y] + + var path, + i = crosshairsOptions.length, + attribs, + axis; + + while (i--) { + if (crosshairsOptions[i] && (axis = point.series[i ? 'yAxis' : 'xAxis'])) { + path = axis + .getPlotLinePath(point[i ? 'y' : 'x'], 1); + if (crosshairs[i]) { + crosshairs[i].attr({ d: path, visibility: VISIBLE }); + + } else { + attribs = { + 'stroke-width': crosshairsOptions[i].width || 1, + stroke: crosshairsOptions[i].color || '#C0C0C0', + zIndex: 2 + }; + if (crosshairsOptions[i].dashStyle) { + attribs.dashstyle = crosshairsOptions[i].dashStyle; + } + crosshairs[i] = renderer.path(path) + .attr(attribs) + .add(); + } + } + } + } + } + + + + // public members + return { + shared: shared, + refresh: refresh, + hide: hide + }; + } + + /** + * The mouse tracker object + * @param {Object} chart + * @param {Object} options + */ + function MouseTracker (chart, options) { + + + var mouseDownX, + mouseDownY, + hasDragged, + selectionMarker, + zoomType = optionsChart.zoomType, + zoomX = /x/.test(zoomType), + zoomY = /y/.test(zoomType), + zoomHor = zoomX && !inverted || zoomY && inverted, + zoomVert = zoomY && !inverted || zoomX && inverted; + + /** + * Add crossbrowser support for chartX and chartY + * @param {Object} e The event object in standard browsers + */ + function normalizeMouseEvent(e) { + var ePos; + + // common IE normalizing + e = e || win.event; + if (!e.target) { + e.target = e.srcElement; + } + + // iOS + ePos = e.touches ? e.touches.item(0) : e; + + // in certain cases, get mouse position + if (e.type != 'mousemove' || win.opera) { // only Opera needs position on mouse move, see below + chartPosition = getPosition(container); + } + + // chartX and chartY + if (isIE) { // IE including IE9 that has chartX but in a different meaning + e.chartX = e.x; + e.chartY = e.y; + } else { + if (ePos.layerX === UNDEFINED) { // Opera and iOS + e.chartX = ePos.pageX - chartPosition.left; + e.chartY = ePos.pageY - chartPosition.top; + } else { + e.chartX = e.layerX; + e.chartY = e.layerY; + } + } + + return e; + } + + /** + * Get the click position in terms of axis values. + * + * @param {Object} e A mouse event + */ + function getMouseCoordinates(e) { + var coordinates = { + xAxis: [], + yAxis: [] + }; + each(axes, function(axis, i) { + var translate = axis.translate, + isXAxis = axis.isXAxis, + isHorizontal = inverted ? !isXAxis : isXAxis; + + coordinates[isXAxis ? 'xAxis' : 'yAxis'].push({ + axis: axis, + value: translate( + isHorizontal ? + e.chartX - plotLeft : + plotHeight - e.chartY + plotTop, + true + ) + }); + }); + return coordinates; + } + + /** + * With line type charts with a single tracker, get the point closest to the mouse + */ + function onmousemove (e) { + var point, + points, + hoverPoint = chart.hoverPoint, + hoverSeries = chart.hoverSeries, + i, + j, + distance = chartWidth, + index = inverted ? e.chartY : e.chartX - plotLeft; // wtf? + + // shared tooltip + if (tooltip && options.shared) { + points = []; + + // loop over all series and find the ones with points closest to the mouse + i = series.length; + for (j = 0; j < i; j++) { + if (series[j].visible && series[j].tooltipPoints.length) { + point = series[j].tooltipPoints[index]; + point._dist = mathAbs(index - point.plotX); + distance = mathMin(distance, point._dist); + points.push(point); + } + } + // remove furthest points + i = points.length; + while (i--) { + if (points[i]._dist > distance) { + points.splice(i, 1); + } + } + // refresh the tooltip if necessary + if (points.length && (points[0].plotX != hoverX)) { + tooltip.refresh(points); + hoverX = points[0].plotX; + } + } + + // separate tooltip and general mouse events + if (hoverSeries && hoverSeries.tracker) { // only use for line-type series with common tracker + + // get the point + point = hoverSeries.tooltipPoints[index]; + + // a new point is hovered, refresh the tooltip + if (point && point != hoverPoint) { + + // trigger the events + point.onMouseOver(); + + } + } + } + + + + /** + * Reset the tracking by hiding the tooltip, the hover series state and the hover point + */ + function resetTracker() { + var hoverSeries = chart.hoverSeries, + hoverPoint = chart.hoverPoint; + + if (hoverPoint) { + hoverPoint.onMouseOut(); + } + + if (hoverSeries) { + hoverSeries.onMouseOut(); + } + + if (tooltip) { + tooltip.hide(); + } + + hoverX = null; + } + + /** + * Mouse up or outside the plot area + */ + function drop() { + if (selectionMarker) { + var selectionData = { + xAxis: [], + yAxis: [] + }, + selectionBox = selectionMarker.getBBox(), + selectionLeft = selectionBox.x - plotLeft, + selectionTop = selectionBox.y - plotTop; + + + // a selection has been made + if (hasDragged) { + + // record each axis' min and max + each(axes, function(axis, i) { + var translate = axis.translate, + isXAxis = axis.isXAxis, + isHorizontal = inverted ? !isXAxis : isXAxis, + selectionMin = translate( + isHorizontal ? + selectionLeft : + plotHeight - selectionTop - selectionBox.height, + true + ), + selectionMax = translate( + isHorizontal ? + selectionLeft + selectionBox.width : + plotHeight - selectionTop, + true + ); + + selectionData[isXAxis ? 'xAxis' : 'yAxis'].push({ + axis: axis, + min: mathMin(selectionMin, selectionMax), // for reversed axes + max: mathMax(selectionMin, selectionMax) + }); + + }); + fireEvent(chart, 'selection', selectionData, zoom); + + } + selectionMarker = selectionMarker.destroy(); + } + + chart.mouseIsDown = mouseIsDown = hasDragged = false; + removeEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop); + + } + + /** + * Set the JS events on the container element + */ + function setDOMEvents () { + var lastWasOutsidePlot = true; + + /* + * Record the starting position of a dragoperation + */ + container.onmousedown = function(e) { + e = normalizeMouseEvent(e); + + // record the start position + //e.preventDefault && e.preventDefault(); + + chart.mouseIsDown = mouseIsDown = true; + mouseDownX = e.chartX; + mouseDownY = e.chartY; + + addEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop); + }; + + // The mousemove, touchmove and touchstart event handler + var mouseMove = function(e) { + + // let the system handle multitouch operations like two finger scroll + // and pinching + if (e && e.touches && e.touches.length > 1) { + return; + } + + // normalize + e = normalizeMouseEvent(e); + if (!hasTouch) { // not for touch devices + e.returnValue = false; + } + + var chartX = e.chartX, + chartY = e.chartY, + isOutsidePlot = !isInsidePlot(chartX - plotLeft, chartY - plotTop); + + // on touch devices, only trigger click if a handler is defined + if (hasTouch && e.type == 'touchstart') { + if (attr(e.target, 'isTracker')) { + if (!chart.runTrackerClick) { + e.preventDefault(); + } + } else if (!runChartClick && !isOutsidePlot) { + e.preventDefault(); + } + } + + // cancel on mouse outside + if (isOutsidePlot) { + + if (!lastWasOutsidePlot) { + // reset the tracker + resetTracker(); + } + + // drop the selection if any and reset mouseIsDown and hasDragged + //drop(); + if (chartX < plotLeft) { + chartX = plotLeft; + } else if (chartX > plotLeft + plotWidth) { + chartX = plotLeft + plotWidth; + } + + if (chartY < plotTop) { + chartY = plotTop; + } else if (chartY > plotTop + plotHeight) { + chartY = plotTop + plotHeight; + } + + } + + if (mouseIsDown && e.type != 'touchstart') { // make selection + + // determine if the mouse has moved more than 10px + if ((hasDragged = Math.sqrt( + Math.pow(mouseDownX - chartX, 2) + + Math.pow(mouseDownY - chartY, 2) + ) > 10)) { + + // make a selection + if (hasCartesianSeries && (zoomX || zoomY) && + isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop)) { + if (!selectionMarker) { + selectionMarker = renderer.rect( + plotLeft, + plotTop, + zoomHor ? 1 : plotWidth, + zoomVert ? 1 : plotHeight, + 0 + ) + .attr({ + fill: 'rgba(69,114,167,0.25)', + zIndex: 7 + }) + .add(); + } + } + + // adjust the width of the selection marker + if (selectionMarker && zoomHor) { + var xSize = chartX - mouseDownX; + selectionMarker.attr({ + width: mathAbs(xSize), + x: (xSize > 0 ? 0 : xSize) + mouseDownX + }); + } + // adjust the height of the selection marker + if (selectionMarker && zoomVert) { + var ySize = chartY - mouseDownY; + selectionMarker.attr({ + height: mathAbs(ySize), + y: (ySize > 0 ? 0 : ySize) + mouseDownY + }); + } + } + + } else if (!isOutsidePlot) { + // show the tooltip + onmousemove(e); + } + + lastWasOutsidePlot = isOutsidePlot; + + // when outside plot, allow touch-drag by returning true + return isOutsidePlot || !hasCartesianSeries; + }; + + /* + * When the mouse enters the container, run mouseMove + */ + container.onmousemove = mouseMove; + + /* + * When the mouse leaves the container, hide the tracking (tooltip). + */ + addEvent(container, 'mouseleave', resetTracker); + + + container.ontouchstart = function(e) { + // For touch devices, use touchmove to zoom + if (zoomX || zoomY) { + container.onmousedown(e); + } + // Show tooltip and prevent the lower mouse pseudo event + mouseMove(e); + }; + + /* + * Allow dragging the finger over the chart to read the values on touch + * devices + */ + container.ontouchmove = mouseMove; + + /* + * Allow dragging the finger over the chart to read the values on touch + * devices + */ + container.ontouchend = function() { + if (hasDragged) { + resetTracker(); + } + }; + + + // MooTools 1.2.3 doesn't fire this in IE when using addEvent + container.onclick = function(e) { + var hoverPoint = chart.hoverPoint; + e = normalizeMouseEvent(e); + + e.cancelBubble = true; // IE specific + + + if (!hasDragged) { + if (hoverPoint && attr(e.target, 'isTracker')) { + var plotX = hoverPoint.plotX, + plotY = hoverPoint.plotY; + + // add page position info + extend(hoverPoint, { + pageX: chartPosition.left + plotLeft + + (inverted ? plotWidth - plotY : plotX), + pageY: chartPosition.top + plotTop + + (inverted ? plotHeight - plotX : plotY) + }); + + // the series click event + fireEvent(hoverPoint.series, 'click', extend(e, { + point: hoverPoint + })); + + // the point click event + hoverPoint.firePointEvent('click', e); + + } else { + extend(e, getMouseCoordinates(e)); + + // fire a click event in the chart + if (isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) { + fireEvent(chart, 'click', e); + } + } + + + } + // reset mouseIsDown and hasDragged + hasDragged = false; + }; + + } + + /** + * Create the image map that listens for mouseovers + */ + placeTrackerGroup = function() { + + // first create - plot positions is not final at this stage + if (!trackerGroup) { + chart.trackerGroup = trackerGroup = renderer.g('tracker') + .attr({ zIndex: 9 }) + .add(); + + // then position - this happens on load and after resizing and changing + // axis or box positions + } else { + trackerGroup.translate(plotLeft, plotTop); + if (inverted) { + trackerGroup.attr({ + width: chart.plotWidth, + height: chart.plotHeight + }).invert(); + } + } + }; + + + // Run MouseTracker + placeTrackerGroup(); + if (options.enabled) { + chart.tooltip = tooltip = Tooltip(options); + } + + setDOMEvents(); + + // set the fixed interval ticking for the smooth tooltip + tooltipInterval = setInterval(function() { + if (tooltipTick) { + tooltipTick(); + } + }, 32); + + // expose properties + extend(this, { + zoomX: zoomX, + zoomY: zoomY, + resetTracker: resetTracker + }); + } + + + + /** + * The overview of the chart's series + * @param {Object} chart + */ + var Legend = function(chart) { + + var options = chart.options.legend; + + if (!options.enabled) { + return; + } + + var horizontal = options.layout == 'horizontal', + symbolWidth = options.symbolWidth, + symbolPadding = options.symbolPadding, + allItems, + style = options.style, + itemStyle = options.itemStyle, + itemHoverStyle = options.itemHoverStyle, + itemHiddenStyle = options.itemHiddenStyle, + padding = pInt(style.padding), + rightPadding = 20, + //lineHeight = options.lineHeight || 16, + y = 18, + initialItemX = 4 + padding + symbolWidth + symbolPadding, + itemX, + itemY, + lastItemY, + itemHeight = 0, + box, + legendBorderWidth = options.borderWidth, + legendBackgroundColor = options.backgroundColor, + legendGroup, + offsetWidth, + widthOption = options.width, + series = chart.series, + reversedLegend = options.reversed; + + + + /** + * Set the colors for the legend item + * @param {Object} item A Series or Point instance + * @param {Object} visible Dimmed or colored + */ + function colorizeItem(item, visible) { + var legendItem = item.legendItem, + legendLine = item.legendLine, + legendSymbol = item.legendSymbol, + hiddenColor = itemHiddenStyle.color, + textColor = visible ? options.itemStyle.color : hiddenColor, + symbolColor = visible ? item.color : hiddenColor; + if (legendItem) { + legendItem.css({ fill: textColor }); + } + if (legendLine) { + legendLine.attr({ stroke: symbolColor }); + } + if (legendSymbol) { + legendSymbol.attr({ + stroke: symbolColor, + fill: symbolColor + }); + } + } + + /** + * Position the legend item + * @param {Object} item A Series or Point instance + * @param {Object} visible Dimmed or colored + */ + function positionItem(item, itemX, itemY) { + var legendItem = item.legendItem, + legendLine = item.legendLine, + legendSymbol = item.legendSymbol, + checkbox = item.checkbox; + if (legendItem) { + legendItem.attr({ + x: itemX, + y: itemY + }); + } + if (legendLine) { + legendLine.translate(itemX, itemY - 4); + } + if (legendSymbol) { + legendSymbol.attr({ + x: itemX + legendSymbol.xOff, + y: itemY + legendSymbol.yOff + }); + } + if (checkbox) { + checkbox.x = itemX; + checkbox.y = itemY; + } + } + + /** + * Destroy a single legend item + * @param {Object} item The series or point + */ + function destroyItem(item) { + var checkbox = item.checkbox; + + // pull out from the array + //erase(allItems, item); + + // destroy SVG elements + each(['legendItem', 'legendLine', 'legendSymbol'], function(key) { + if (item[key]) { + item[key].destroy(); + } + }); + + if (checkbox) { + discardElement(item.checkbox); + } + + + } + + + /** + * Position the checkboxes after the width is determined + */ + function positionCheckboxes() { + each(allItems, function(item) { + var checkbox = item.checkbox; + if (checkbox) { + css(checkbox, { + left: (legendGroup.attr('translateX') + item.legendItemWidth + checkbox.x - 40) +PX, + top: (legendGroup.attr('translateY') + checkbox.y - 11) + PX + }); + } + }); + } + + /** + * Render a single specific legend item + * @param {Object} item A series or point + */ + function renderItem(item) { + var bBox, + itemWidth, + legendSymbol, + symbolX, + symbolY, + attribs, + simpleSymbol, + li = item.legendItem, + series = item.series || item, + i = allItems.length; + + + if (!li) { // generate it once, later move it + + // let these series types use a simple symbol + simpleSymbol = /^(bar|pie|area|column)$/.test(series.type); + + // generate the list item text + item.legendItem = li = renderer.text( + options.labelFormatter.call(item), + 0, + 0 + ) + .css(item.visible ? itemStyle : itemHiddenStyle) + .on('mouseover', function() { + item.setState(HOVER_STATE); + li.css(itemHoverStyle); + }) + .on('mouseout', function() { + li.css(item.visible ? itemStyle : itemHiddenStyle); + item.setState(); + }) + .on('click', function(event) { + var strLegendItemClick = 'legendItemClick', + fnLegendItemClick = function() { + item.setVisible(); + }; + + // click the name or symbol + if (item.firePointEvent) { // point + item.firePointEvent(strLegendItemClick, null, fnLegendItemClick); + } else { + fireEvent(item, strLegendItemClick, null, fnLegendItemClick); + } + }) + .attr({ zIndex: 2 }) + .add(legendGroup); + + // draw the line + if (!simpleSymbol && item.options && item.options.lineWidth) { + var itemOptions = item.options; + attribs = { + 'stroke-width': itemOptions.lineWidth, + zIndex: 2 + }; + if (itemOptions.dashStyle) { + attribs.dashstyle = itemOptions.dashStyle; + } + item.legendLine = renderer.path([ + M, + -symbolWidth - symbolPadding, + 0, + L, + -symbolPadding, + 0 + ]) + .attr(attribs) + .add(legendGroup); + } + + // draw a simple symbol + if (simpleSymbol) { // bar|pie|area|column + //legendLayer.drawRect( + legendSymbol = renderer.rect( + (symbolX = -symbolWidth - symbolPadding), + (symbolY = -11), + symbolWidth, + 12, + 2 + ).attr({ + 'stroke-width': 0, + zIndex: 3 + }).add(legendGroup); + } + + // draw the marker + else if (item.options && item.options.marker && item.options.marker.enabled) { + legendSymbol = renderer.symbol( + item.symbol, + (symbolX = -symbolWidth / 2 - symbolPadding), + (symbolY = -4), + item.options.marker.radius + ) + .attr(item.pointAttr[NORMAL_STATE]) + .attr({ zIndex: 3 }) + .add(legendGroup); + + + } + if (legendSymbol) { + legendSymbol.xOff = symbolX; + legendSymbol.yOff = symbolY; + } + + item.legendSymbol = legendSymbol; + + // colorize the items + colorizeItem(item, item.visible); + + + // add the HTML checkbox on top + if (item.options && item.options.showCheckbox) { + item.checkbox = createElement('input', { + type: 'checkbox', + checked: item.selected, + defaultChecked: item.selected // required by IE7 + }, options.itemCheckboxStyle, container); + + addEvent(item.checkbox, 'click', function(event) { + var target = event.target; + fireEvent(item, 'checkboxClick', { + checked: target.checked + }, + function() { + item.select(); + } + ); + }); + } + } + + + // calculate the positions for the next line + bBox = li.getBBox(); + + itemWidth = item.legendItemWidth = + options.itemWidth || symbolWidth + symbolPadding + bBox.width + rightPadding; + itemHeight = bBox.height; + + // if the item exceeds the width, start a new line + if (horizontal && itemX - initialItemX + itemWidth > + (widthOption || (chartWidth - 2 * padding - initialItemX))) { + itemX = initialItemX; + itemY += itemHeight; + } + lastItemY = itemY; + + // position the newly generated or reordered items + positionItem(item, itemX, itemY); + + // advance + if (horizontal) { + itemX += itemWidth; + } else { + itemY += itemHeight; + } + + // the width of the widest item + offsetWidth = widthOption || mathMax( + horizontal ? itemX - initialItemX : itemWidth, + offsetWidth + ); + + + + // add it all to an array to use below + allItems.push(item); + } + + /** + * Render the legend. This method can be called both before and after + * chart.render. If called after, it will only rearrange items instead + * of creating new ones. + */ + function renderLegend() { + itemX = initialItemX; + itemY = y; + offsetWidth = 0; + lastItemY = 0; + + allItems = []; + + if (!legendGroup) { + legendGroup = renderer.g('legend') + .attr({ zIndex: 7 }) + .add(); + } + + + // add HTML for each series + if (reversedLegend) { + series.reverse(); + } + each(series, function(serie) { + if (!serie.options.showInLegend) { + return; + } + + // use points or series for the legend item depending on legendType + var items = (serie.options.legendType == 'point') ? + serie.data : [serie]; + + // render all items + each(items, renderItem); + }); + if (reversedLegend) { // restore + series.reverse(); + } + + + + // Draw the border + legendWidth = widthOption || offsetWidth; + legendHeight = lastItemY - y + itemHeight; + + if (legendBorderWidth || legendBackgroundColor) { + legendWidth += 2 * padding; + legendHeight += 2 * padding; + + if (!box) { + box = renderer.rect( + 0, + 0, + legendWidth, + legendHeight, + options.borderRadius, + legendBorderWidth || 0 + ).attr({ + stroke: options.borderColor, + 'stroke-width': legendBorderWidth || 0, + fill: legendBackgroundColor || NONE + }) + .add(legendGroup) + .shadow(options.shadow); + + } else if (legendWidth > 0 && legendHeight > 0) { + box.animate( + box.crisp(null, null, null, legendWidth, legendHeight) + ); + } + + // hide the border if no items + box[allItems.length ? 'show' : 'hide'](); + } + + // 1.x compatibility: positioning based on style + var props = ['left', 'right', 'top', 'bottom'], + prop, + i = 4; + while(i--) { + prop = props[i]; + if (style[prop] && style[prop] != 'auto') { + options[i < 2 ? 'align' : 'verticalAlign'] = prop; + options[i < 2 ? 'x' : 'y'] = pInt(style[prop]) * (i % 2 ? -1 : 1); + } + } + + legendGroup.align(extend(options, { + width: legendWidth, + height: legendHeight + }), true, spacingBox); + + if (!isResizing) { + positionCheckboxes(); + } + } + + + // run legend + renderLegend(); + + // move checkboxes + addEvent(chart, 'endResize', positionCheckboxes); + + // expose + return { + colorizeItem: colorizeItem, + destroyItem: destroyItem, + renderLegend: renderLegend + }; + }; + + + + + + + /** + * Initialize an individual series, called internally before render time + */ + function initSeries(options) { + var type = options.type || optionsChart.type || optionsChart.defaultSeriesType, + typeClass = seriesTypes[type], + serie, + hasRendered = chart.hasRendered; + + // an inverted chart can't take a column series and vice versa + if (hasRendered) { + if (inverted && type == 'column') { + typeClass = seriesTypes.bar; + } else if (!inverted && type == 'bar') { + typeClass = seriesTypes.column; + } + } + + serie = new typeClass(); + + serie.init(chart, options); + + // set internal chart properties + if (!hasRendered && serie.inverted) { + inverted = true; + } + if (serie.isCartesian) { + hasCartesianSeries = serie.isCartesian; + } + + series.push(serie); + + return serie; + } + + /** + * Add a series dynamically after time + * + * @param {Object} options The config options + * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true. + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + * + * @return {Object} series The newly created series object + */ + function addSeries(options, redraw, animation) { + var series; + + if (options) { + setAnimation(animation, chart); + redraw = pick(redraw, true); // defaults to true + + fireEvent(chart, 'addSeries', { options: options }, function() { + series = initSeries(options); + series.isDirty = true; + + chart.isDirtyLegend = true; // the series array is out of sync with the display + if (redraw) { + chart.redraw(); + } + }); + } + + return series; + } + + /** + * Check whether a given point is within the plot area + * + * @param {Number} x Pixel x relative to the coordinateSystem + * @param {Number} y Pixel y relative to the coordinateSystem + */ + isInsidePlot = function(x, y) { + return x >= 0 && + x <= plotWidth && + y >= 0 && + y <= plotHeight; + }; + + /** + * Adjust all axes tick amounts + */ + function adjustTickAmounts() { + if (optionsChart.alignTicks !== false) { + each(axes, function(axis) { + axis.adjustTickAmount(); + }); + } + maxTicks = null; + } + + /** + * Redraw legend, axes or series based on updated data + * + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + function redraw(animation) { + var redrawLegend = chart.isDirtyLegend, + hasStackedSeries, + isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed? + seriesLength = series.length, + i = seriesLength, + clipRect = chart.clipRect, + serie; + + setAnimation(animation, chart); + + // link stacked series + while (i--) { + serie = series[i]; + if (serie.isDirty && serie.options.stacking) { + hasStackedSeries = true; + break; + } + } + if (hasStackedSeries) { // mark others as dirty + i = seriesLength; + while (i--) { + serie = series[i]; + if (serie.options.stacking) { + serie.isDirty = true; + } + } + } + + // handle updated data in the series + each(series, function(serie) { + if (serie.isDirty) { // prepare the data so axis can read it + serie.cleanData(); + serie.getSegments(); + + if (serie.options.legendType == 'point') { + redrawLegend = true; + } + } + }); + + // handle added or removed series + if (redrawLegend && legend.renderLegend) { // series or pie points are added or removed + // draw legend graphics + legend.renderLegend(); + + chart.isDirtyLegend = false; + } + + if (hasCartesianSeries) { + if (!isResizing) { + + // reset maxTicks + maxTicks = null; + + // set axes scales + each(axes, function(axis) { + axis.setScale(); + }); + } + adjustTickAmounts(); + getMargins(); + + // redraw axes + each(axes, function(axis) { + if (axis.isDirty || isDirtyBox) { + axis.redraw(); + isDirtyBox = true; // always redraw box to reflect changes in the axis labels + } + }); + + + } + + // the plot areas size has changed + if (isDirtyBox) { + drawChartBox(); + placeTrackerGroup(); + + // move clip rect + if (clipRect) { + stop(clipRect); + clipRect.animate({ // for chart resize + width: chart.plotSizeX, + height: chart.plotSizeY + }); + } + + } + + + // redraw affected series + each(series, function(serie) { + if (serie.isDirty && serie.visible && + (!serie.isCartesian || serie.xAxis)) { // issue #153 + serie.redraw(); + } + }); + + + // hide tooltip and hover states + if (tracker && tracker.resetTracker) { + tracker.resetTracker(); + } + + // fire the event + fireEvent(chart, 'redraw'); + } + + + + /** + * Dim the chart and show a loading text or symbol + * @param {String} str An optional text to show in the loading label instead of the default one + */ + function showLoading(str) { + var loadingOptions = options.loading; + + // create the layer at the first call + if (!loadingDiv) { + loadingDiv = createElement(DIV, { + className: 'highcharts-loading' + }, extend(loadingOptions.style, { + left: plotLeft + PX, + top: plotTop + PX, + width: plotWidth + PX, + height: plotHeight + PX, + zIndex: 10, + display: NONE + }), container); + + loadingSpan = createElement( + 'span', + null, + loadingOptions.labelStyle, + loadingDiv + ); + + } + + // update text + loadingSpan.innerHTML = str || options.lang.loading; + + // show it + if (!loadingShown) { + css(loadingDiv, { opacity: 0, display: '' }); + animate(loadingDiv, { + opacity: loadingOptions.style.opacity + }, { + duration: loadingOptions.showDuration + }); + loadingShown = true; + } + } + /** + * Hide the loading layer + */ + function hideLoading() { + animate(loadingDiv, { + opacity: 0 + }, { + duration: options.loading.hideDuration, + complete: function() { + css(loadingDiv, { display: NONE }); + } + }); + loadingShown = false; + } + + /** + * Get an axis, series or point object by id. + * @param id {String} The id as given in the configuration options + */ + function get(id) { + var i, + j, + data; + + // search axes + for (i = 0; i < axes.length; i++) { + if (axes[i].options.id == id) { + return axes[i]; + } + } + + // search series + for (i = 0; i < series.length; i++) { + if (series[i].options.id == id) { + return series[i]; + } + } + + // search points + for (i = 0; i < series.length; i++) { + data = series[i].data; + for (j = 0; j < data.length; j++) { + if (data[j].id == id) { + return data[j]; + } + } + } + return null; + } + + /** + * Create the Axis instances based on the config options + */ + function getAxes() { + var xAxisOptions = options.xAxis || {}, + yAxisOptions = options.yAxis || {}, + axis; + + // make sure the options are arrays and add some members + xAxisOptions = splat(xAxisOptions); + each(xAxisOptions, function(axis, i) { + axis.index = i; + axis.isX = true; + }); + + yAxisOptions = splat(yAxisOptions); + each(yAxisOptions, function(axis, i) { + axis.index = i; + }); + + // concatenate all axis options into one array + axes = xAxisOptions.concat(yAxisOptions); + + // loop the options and construct axis objects + chart.xAxis = []; + chart.yAxis = []; + axes = map(axes, function(axisOptions) { + axis = new Axis(chart, axisOptions); + chart[axis.isXAxis ? 'xAxis' : 'yAxis'].push(axis); + + return axis; + }); + + adjustTickAmounts(); + } + + + /** + * Get the currently selected points from all series + */ + function getSelectedPoints() { + var points = []; + each(series, function(serie) { + points = points.concat( grep( serie.data, function(point) { + return point.selected; + })); + }); + return points; + } + + /** + * Get the currently selected series + */ + function getSelectedSeries() { + return grep(series, function (serie) { + return serie.selected; + }); + } + + /** + * Zoom out to 1:1 + */ + zoomOut = function () { + fireEvent(chart, 'selection', { resetSelection: true }, zoom); + chart.toolbar.remove('zoom'); + + }; + /** + * Zoom into a given portion of the chart given by axis coordinates + * @param {Object} event + */ + zoom = function (event) { + + // add button to reset selection + var lang = defaultOptions.lang, + animate = chart.pointCount < 100; + chart.toolbar.add('zoom', lang.resetZoom, lang.resetZoomTitle, zoomOut); + + // if zoom is called with no arguments, reset the axes + if (!event || event.resetSelection) { + each(axes, function(axis) { + axis.setExtremes(null, null, false, animate); + }); + } + + // else, zoom in on all axes + else { + each(event.xAxis.concat(event.yAxis), function(axisData) { + var axis = axisData.axis; + + // don't zoom more than maxZoom + if (chart.tracker[axis.isXAxis ? 'zoomX' : 'zoomY']) { + axis.setExtremes(axisData.min, axisData.max, false, animate); + } + }); + } + + // redraw chart + redraw(); + }; + + /** + * Show the title and subtitle of the chart + * + * @param titleOptions {Object} New title options + * @param subtitleOptions {Object} New subtitle options + * + */ + function setTitle (titleOptions, subtitleOptions) { + + chartTitleOptions = merge(options.title, titleOptions); + chartSubtitleOptions = merge(options.subtitle, subtitleOptions); + + // add title and subtitle + each([ + ['title', titleOptions, chartTitleOptions], + ['subtitle', subtitleOptions, chartSubtitleOptions] + ], function(arr) { + var name = arr[0], + title = chart[name], + titleOptions = arr[1], + chartTitleOptions = arr[2]; + + if (title && titleOptions) { + title.destroy(); // remove old + title = null; + } + if (chartTitleOptions && chartTitleOptions.text && !title) { + chart[name] = renderer.text( + chartTitleOptions.text, + 0, + 0 + ) + .attr({ + align: chartTitleOptions.align, + 'class': 'highcharts-'+ name, + zIndex: 1 + }) + .css(chartTitleOptions.style) + .add() + .align(chartTitleOptions, false, spacingBox); + } + }); + + } + + /** + * Get chart width and height according to options and container size + */ + function getChartSize() { + + containerWidth = (renderToClone || renderTo).offsetWidth; + containerHeight = (renderToClone || renderTo).offsetHeight; + chart.chartWidth = chartWidth = optionsChart.width || containerWidth || 600; + chart.chartHeight = chartHeight = optionsChart.height || + // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7: + (containerHeight > 19 ? containerHeight : 400); + } + + + /** + * Get the containing element, determine the size and create the inner container + * div to hold the chart + */ + function getContainer() { + renderTo = optionsChart.renderTo; + containerId = PREFIX + idCounter++; + + if (isString(renderTo)) { + renderTo = doc.getElementById(renderTo); + } + + // remove previous chart + renderTo.innerHTML = ''; + + // If the container doesn't have an offsetWidth, it has or is a child of a node + // that has display:none. We need to temporarily move it out to a visible + // state to determine the size, else the legend and tooltips won't render + // properly + if (!renderTo.offsetWidth) { + renderToClone = renderTo.cloneNode(0); + css(renderToClone, { + position: ABSOLUTE, + top: '-9999px', + display: '' + }); + doc.body.appendChild(renderToClone); + } + + // get the width and height + getChartSize(); + + // create the inner container + chart.container = container = createElement(DIV, { + className: 'highcharts-container' + + (optionsChart.className ? ' '+ optionsChart.className : ''), + id: containerId + }, extend({ + position: RELATIVE, + overflow: HIDDEN, // needed for context menu (avoid scrollbars) and + // content overflow in IE + width: chartWidth + PX, + height: chartHeight + PX, + textAlign: 'left' + }, optionsChart.style), + renderToClone || renderTo + ); + + chart.renderer = renderer = + optionsChart.forExport ? // force SVG, used for SVG export + new SVGRenderer(container, chartWidth, chartHeight, true) : + new Renderer(container, chartWidth, chartHeight); + + // Issue 110 workaround: + // In Firefox, if a div is positioned by percentage, its pixel position may land + // between pixels. The container itself doesn't display this, but an SVG element + // inside this container will be drawn at subpixel precision. In order to draw + // sharp lines, this must be compensated for. This doesn't seem to work inside + // iframes though (like in jsFiddle). + var subPixelFix, rect; + if (isFirefox && container.getBoundingClientRect) { + subPixelFix = function() { + css(container, { left: 0, top: 0 }); + rect = container.getBoundingClientRect(); + css(container, { + left: (-rect.left % 1) + PX, + top: (-rect.top % 1) + PX + }); + }; + + // run the fix now + subPixelFix(); + + // run it on resize + addEvent(win, 'resize', subPixelFix); + + // remove it on chart destroy + addEvent(chart, 'destroy', function() { + removeEvent(win, 'resize', subPixelFix); + }); + } + } + + /** + * Calculate margins by rendering axis labels in a preliminary position. Title, + * subtitle and legend have already been rendered at this stage, but will be + * moved into their final positions + */ + getMargins = function() { + var legendOptions = options.legend, + legendMargin = pick(legendOptions.margin, 10), + legendX = legendOptions.x, + legendY = legendOptions.y, + align = legendOptions.align, + verticalAlign = legendOptions.verticalAlign, + titleOffset; + + resetMargins(); + + // adjust for title and subtitle + if ((chart.title || chart.subtitle) && !defined(optionsMarginTop)) { + titleOffset = mathMax( + chart.title && !chartTitleOptions.floating && !chartTitleOptions.verticalAlign && chartTitleOptions.y || 0, + chart.subtitle && !chartSubtitleOptions.floating && !chartSubtitleOptions.verticalAlign && chartSubtitleOptions.y || 0 + ); + if (titleOffset) { + plotTop = mathMax(plotTop, titleOffset + pick(chartTitleOptions.margin, 15) + spacingTop); + } + } + // adjust for legend + if (legendOptions.enabled && !legendOptions.floating) { + if (align == 'right') { // horizontal alignment handled first + if (!defined(optionsMarginRight)) { + marginRight = mathMax( + marginRight, + legendWidth - legendX + legendMargin + spacingRight + ); + } + } else if (align == 'left') { + if (!defined(optionsMarginLeft)) { + plotLeft = mathMax( + plotLeft, + legendWidth + legendX + legendMargin + spacingLeft + ); + } + + } else if (verticalAlign == 'top') { + if (!defined(optionsMarginTop)) { + plotTop = mathMax( + plotTop, + legendHeight + legendY + legendMargin + spacingTop + ); + } + + } else if (verticalAlign == 'bottom') { + if (!defined(optionsMarginBottom)) { + marginBottom = mathMax( + marginBottom, + legendHeight - legendY + legendMargin + spacingBottom + ); + } + } + } + + // pre-render axes to get labels offset width + if (hasCartesianSeries) { + each(axes, function(axis) { + axis.getOffset(); + }); + } + + if (!defined(optionsMarginLeft)) { + plotLeft += axisOffset[3]; + } + if (!defined(optionsMarginTop)) { + plotTop += axisOffset[0]; + } + if (!defined(optionsMarginBottom)) { + marginBottom += axisOffset[2]; + } + if (!defined(optionsMarginRight)) { + marginRight += axisOffset[1]; + } + + setChartSize(); + + }; + + /** + * Add the event handlers necessary for auto resizing + * + */ + function initReflow() { + var reflowTimeout; + function reflow() { + var width = optionsChart.width || renderTo.offsetWidth, + height = optionsChart.height || renderTo.offsetHeight; + + if (width && height) { // means container is display:none + if (width != containerWidth || height != containerHeight) { + clearTimeout(reflowTimeout); + reflowTimeout = setTimeout(function() { + resize(width, height, false); + }, 100); + } + containerWidth = width; + containerHeight = height; + } + } + addEvent(window, 'resize', reflow); + addEvent(chart, 'destroy', function() { + removeEvent(window, 'resize', reflow); + }); + } + + /** + * Resize the chart to a given width and height + * @param {Number} width + * @param {Number} height + * @param {Object|Boolean} animation + */ + resize = function(width, height, animation) { + var chartTitle = chart.title, + chartSubtitle = chart.subtitle; + + isResizing += 1; + + // set the animation for the current process + setAnimation(animation, chart); + + oldChartHeight = chartHeight; + oldChartWidth = chartWidth; + chartWidth = mathRound(width); + chartHeight = mathRound(height); + + css(container, { + width: chartWidth + PX, + height: chartHeight + PX + }); + renderer.setSize(chartWidth, chartHeight, animation); + + // update axis lengths for more correct tick intervals: + plotWidth = chartWidth - plotLeft - marginRight; + plotHeight = chartHeight - plotTop - marginBottom; + + // handle axes + maxTicks = null; + each(axes, function(axis) { + axis.isDirty = true; + axis.setScale(); + }); + + // make sure non-cartesian series are also handled + each(series, function(serie) { + serie.isDirty = true; + }); + + chart.isDirtyLegend = true; // force legend redraw + chart.isDirtyBox = true; // force redraw of plot and chart border + + getMargins(); + + // move titles + if (chartTitle) { + chartTitle.align(null, null, spacingBox); + } + if (chartSubtitle) { + chartSubtitle.align(null, null, spacingBox); + } + + redraw(animation); + + + oldChartHeight = null; + fireEvent(chart, 'resize'); + + // fire endResize and set isResizing back + setTimeout(function() { + fireEvent(chart, 'endResize', null, function() { + isResizing -= 1; + }); + }, globalAnimation && globalAnimation.duration || 500); + }; + + /** + * Set the public chart properties. This is done before and after the pre-render + * to determine margin sizes + */ + setChartSize = function() { + + chart.plotLeft = plotLeft = mathRound(plotLeft); + chart.plotTop = plotTop = mathRound(plotTop); + chart.plotWidth = plotWidth = mathRound(chartWidth - plotLeft - marginRight); + chart.plotHeight = plotHeight = mathRound(chartHeight - plotTop - marginBottom); + + chart.plotSizeX = inverted ? plotHeight : plotWidth; + chart.plotSizeY = inverted ? plotWidth : plotHeight; + + spacingBox = { + x: spacingLeft, + y: spacingTop, + width: chartWidth - spacingLeft - spacingRight, + height: chartHeight - spacingTop - spacingBottom + }; + }; + + /** + * Initial margins before auto size margins are applied + */ + resetMargins = function() { + plotTop = pick(optionsMarginTop, spacingTop); + marginRight = pick(optionsMarginRight, spacingRight); + marginBottom = pick(optionsMarginBottom, spacingBottom); + plotLeft = pick(optionsMarginLeft, spacingLeft); + axisOffset = [0, 0, 0, 0]; // top, right, bottom, left + }; + + /** + * Draw the borders and backgrounds for chart and plot area + */ + drawChartBox = function() { + var chartBorderWidth = optionsChart.borderWidth || 0, + chartBackgroundColor = optionsChart.backgroundColor, + plotBackgroundColor = optionsChart.plotBackgroundColor, + plotBackgroundImage = optionsChart.plotBackgroundImage, + mgn, + plotSize = { + x: plotLeft, + y: plotTop, + width: plotWidth, + height: plotHeight + }; + + // Chart area + mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0); + + if (chartBorderWidth || chartBackgroundColor) { + if (!chartBackground) { + chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn, + optionsChart.borderRadius, chartBorderWidth) + .attr({ + stroke: optionsChart.borderColor, + 'stroke-width': chartBorderWidth, + fill: chartBackgroundColor || NONE + }) + .add() + .shadow(optionsChart.shadow); + } else { // resize + chartBackground.animate( + chartBackground.crisp(null, null, null, chartWidth - mgn, chartHeight - mgn) + ); + } + } + + + // Plot background + if (plotBackgroundColor) { + if (!plotBackground) { + plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0) + .attr({ + fill: plotBackgroundColor + }) + .add() + .shadow(optionsChart.plotShadow); + } else { + plotBackground.animate(plotSize); + } + } + if (plotBackgroundImage) { + if (!plotBGImage) { + plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight) + .add(); + } else { + plotBGImage.animate(plotSize); + } + } + + // Plot area border + if (optionsChart.plotBorderWidth) { + if (!plotBorder) { + plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, optionsChart.plotBorderWidth) + .attr({ + stroke: optionsChart.plotBorderColor, + 'stroke-width': optionsChart.plotBorderWidth, + zIndex: 4 + }) + .add(); + } else { + plotBorder.animate( + plotBorder.crisp(null, plotLeft, plotTop, plotWidth, plotHeight) + ); + } + } + + // reset + chart.isDirtyBox = false; + }; + + /** + * Render all graphics for the chart + */ + function render () { + var labels = options.labels, + credits = options.credits, + creditsHref; + + // Title + setTitle(); + + + // Legend + legend = chart.legend = new Legend(chart); + + // Get margins by pre-rendering axes + getMargins(); + each(axes, function(axis) { + axis.setTickPositions(true); // update to reflect the new margins + }); + adjustTickAmounts(); + getMargins(); // second pass to check for new labels + + + // Draw the borders and backgrounds + drawChartBox(); + + // Axes + if (hasCartesianSeries) { + each(axes, function(axis) { + axis.render(); + }); + } + + + // The series + if (!chart.seriesGroup) { + chart.seriesGroup = renderer.g('series-group') + .attr({ zIndex: 3 }) + .add(); + } + each(series, function(serie) { + serie.translate(); + serie.setTooltipPoints(); + serie.render(); + }); + + + // Labels + if (labels.items) { + each(labels.items, function() { + var style = extend(labels.style, this.style), + x = pInt(style.left) + plotLeft, + y = pInt(style.top) + plotTop + 12; + + // delete to prevent rewriting in IE + delete style.left; + delete style.top; + + renderer.text( + this.html, + x, + y + ) + .attr({ zIndex: 2 }) + .css(style) + .add(); + + }); + } + + // Toolbar (don't redraw) + if (!chart.toolbar) { + chart.toolbar = Toolbar(chart); + } + + // Credits + if (credits.enabled && !chart.credits) { + creditsHref = credits.href; + renderer.text( + credits.text, + 0, + 0 + ) + .on('click', function() { + if (creditsHref) { + location.href = creditsHref; + } + }) + .attr({ + align: credits.position.align, + zIndex: 8 + }) + .css(credits.style) + .add() + .align(credits.position); + } + + placeTrackerGroup(); + + // Set flag + chart.hasRendered = true; + + // If the chart was rendered outside the top container, put it back in + if (renderToClone) { + renderTo.appendChild(container); + discardElement(renderToClone); + //updatePosition(container); + } + } + + /** + * Clean up memory usage + */ + function destroy() { + var i = series.length, + parentNode = container && container.parentNode; + + // fire the chart.destoy event + fireEvent(chart, 'destroy'); + + // remove events + removeEvent(win, 'unload', destroy); + removeEvent(chart); + + each(axes, function(axis) { + removeEvent(axis); + }); + + // destroy each series + while (i--) { + series[i].destroy(); + } + + // remove container and all SVG + if (container) { // can break in IE when destroyed before finished loading + container.innerHTML = ''; + removeEvent(container); + if (parentNode) { + parentNode.removeChild(container); + } + + // IE6 leak + container = null; + } + + // IE7 leak + if (renderer) { // can break in IE when destroyed before finished loading + renderer.alignedObjects = null; + } + + // memory and CPU leak + clearInterval(tooltipInterval); + + // clean it all up + for (i in chart) { + delete chart[i]; + } + + } + /** + * Prepare for first rendering after all data are loaded + */ + function firstRender() { + + // VML namespaces can't be added until after complete. Listening + // for Perini's doScroll hack is not enough. + var onreadystatechange = 'onreadystatechange'; + if (!hasSVG && win == win.top && doc.readyState != 'complete') { + doc.attachEvent(onreadystatechange, function() { + doc.detachEvent(onreadystatechange, firstRender); + firstRender(); + }); + return; + } + + // create the container + getContainer(); + + resetMargins(); + setChartSize(); + + // Initialize the series + each(options.series || [], function(serieOptions) { + initSeries(serieOptions); + }); + + // Set the common inversion and transformation for inverted series after initSeries + chart.inverted = inverted = pick(inverted, options.chart.inverted); + + + getAxes(); + + + chart.render = render; + + // depends on inverted and on margins being set + chart.tracker = tracker = new MouseTracker(chart, options.tooltip); + + //globalAnimation = false; + render(); + + fireEvent(chart, 'load'); + + //globalAnimation = true; + + // run callbacks + if (callback) { + callback.apply(chart, [chart]); + } + each(chart.callbacks, function(fn) { + fn.apply(chart, [chart]); + }); + } + + // Run chart + + + + // Set to zero for each new chart + colorCounter = 0; + symbolCounter = 0; + + // Destroy the chart and free up memory. + addEvent(win, 'unload', destroy); + + // Set up auto resize + if (optionsChart.reflow !== false) { + addEvent(chart, 'load', initReflow); + } + + // Chart event handlers + if (chartEvents) { + for (eventType in chartEvents) { + addEvent(chart, eventType, chartEvents[eventType]); + } + } + + + chart.options = options; + chart.series = series; + + + + + + + // Expose methods and variables + chart.addSeries = addSeries; + chart.animation = pick(optionsChart.animation, true); + chart.destroy = destroy; + chart.get = get; + chart.getSelectedPoints = getSelectedPoints; + chart.getSelectedSeries = getSelectedSeries; + chart.hideLoading = hideLoading; + chart.isInsidePlot = isInsidePlot; + chart.redraw = redraw; + chart.setSize = resize; + chart.setTitle = setTitle; + chart.showLoading = showLoading; + chart.pointCount = 0; + /* + if ($) $(function() { + $container = $('#container'); + var origChartWidth, + origChartHeight; + if ($container) { + $('') + .insertBefore($container) + .click(function() { + if (origChartWidth === UNDEFINED) { + origChartWidth = chartWidth; + origChartHeight = chartHeight; + } + chart.resize(chartWidth *= 1.1, chartHeight *= 1.1); + }); + $('') + .insertBefore($container) + .click(function() { + if (origChartWidth === UNDEFINED) { + origChartWidth = chartWidth; + origChartHeight = chartHeight; + } + chart.resize(chartWidth *= 0.9, chartHeight *= 0.9); + }); + $('') + .insertBefore($container) + .click(function() { + if (origChartWidth === UNDEFINED) { + origChartWidth = chartWidth; + origChartHeight = chartHeight; + } + chart.resize(origChartWidth, origChartHeight); + }); + } + }) + */ + + + + + firstRender(); + + +} // end Chart + +// Hook for exporting module +Chart.prototype.callbacks = []; + +/** + * The Point object and prototype. Inheritable and used as base for PiePoint + */ +var Point = function() {}; +Point.prototype = { + + /** + * Initialize the point + * @param {Object} series The series object containing this point + * @param {Object} options The data in either number, array or object format + */ + init: function(series, options) { + var point = this, + defaultColors; + point.series = series; + point.applyOptions(options); + point.pointAttr = {}; + + if (series.options.colorByPoint) { + defaultColors = series.chart.options.colors; + if (!point.options) { + point.options = {}; + } + point.color = point.options.color = point.color || defaultColors[colorCounter++]; + + // loop back to zero + if (colorCounter >= defaultColors.length) { + colorCounter = 0; + } + } + + series.chart.pointCount++; + return point; + }, + /** + * Apply the options containing the x and y data and possible some extra properties. + * This is called on point init or from point.update. + * + * @param {Object} options + */ + applyOptions: function(options) { + var point = this, + series = point.series; + + point.config = options; + + // onedimensional array input + if (isNumber(options) || options === null) { + point.y = options; + } + + // object input + else if (isObject(options) && !isNumber(options.length)) { + + // copy options directly to point + extend(point, options); + point.options = options; + } + + // categorized data with name in first position + else if (isString(options[0])) { + point.name = options[0]; + point.y = options[1]; + } + + // two-dimentional array + else if (isNumber(options[0])) { + point.x = options[0]; + point.y = options[1]; + } + + /* + * If no x is set by now, get auto incremented value. All points must have an + * x value, however the y value can be null to create a gap in the series + */ + if (point.x === UNDEFINED) { + point.x = series.autoIncrement(); + } + + }, + + /** + * Destroy a point to clear memory. Its reference still stays in series.data. + */ + destroy: function() { + var point = this, + series = point.series, + prop; + + series.chart.pointCount--; + + if (point == series.chart.hoverPoint) { + point.onMouseOut(); + } + series.chart.hoverPoints = null; // remove reference + + // remove all events + removeEvent(point); + + each(['graphic', 'tracker', 'group', 'dataLabel', 'connector'], function(prop) { + if (point[prop]) { + point[prop].destroy(); + } + }); + + if (point.legendItem) { // pies have legend items + point.series.chart.legend.destroyItem(point); + } + + for (prop in point) { + point[prop] = null; + } + + + }, + + /** + * Toggle the selection status of a point + * @param {Boolean} selected Whether to select or unselect the point. + * @param {Boolean} accumulate Whether to add to the previous selection. By default, + * this happens if the control key (Cmd on Mac) was pressed during clicking. + */ + select: function(selected, accumulate) { + var point = this, + series = point.series, + chart = series.chart; + + point.selected = selected = pick(selected, !point.selected); + + //series.isDirty = true; + point.firePointEvent(selected ? 'select' : 'unselect'); + point.setState(selected && SELECT_STATE); + + // unselect all other points unless Ctrl or Cmd + click + if (!accumulate) { + each(chart.getSelectedPoints(), function (loopPoint) { + if (loopPoint.selected && loopPoint != point) { + loopPoint.selected = false; + loopPoint.setState(NORMAL_STATE); + loopPoint.firePointEvent('unselect'); + } + }); + } + + }, + + onMouseOver: function() { + var point = this, + chart = point.series.chart, + tooltip = chart.tooltip, + hoverPoint = chart.hoverPoint; + + // set normal state to previous series + if (hoverPoint && hoverPoint != point) { + hoverPoint.onMouseOut(); + } + + // trigger the event + point.firePointEvent('mouseOver'); + + // update the tooltip + if (tooltip && !tooltip.shared) { + tooltip.refresh(point); + } + + // hover this + point.setState(HOVER_STATE); + chart.hoverPoint = point; + }, + + onMouseOut: function() { + var point = this; + point.firePointEvent('mouseOut'); + + point.setState(); + point.series.chart.hoverPoint = null; + }, + + /** + * Extendable method for formatting each point's tooltip line + * + * @param {Boolean} useHeader Whether a common header is used for multiple series in the tooltip + * + * @return {String} A string to be concatenated in to the common tooltip text + */ + tooltipFormatter: function(useHeader) { + var point = this, + series = point.series; + + return ['', (point.name || series.name), ': ', + (!useHeader ? ('x = '+ (point.name || point.x) + ', ') : ''), + '', (!useHeader ? 'y = ' : '' ), point.y, '
'].join(''); + + }, + + /** + * Get the formatted text for this point's data label + * + * @return {String} The formatted data label pseudo-HTML + */ + getDataLabelText: function() { + var point = this; + return this.series.options.dataLabels.formatter.call({ + x: point.x, + y: point.y, + series: point.series, + point: point, + percentage: point.percentage, + total: point.total || point.stackTotal + }); + }, + + /** + * Update the point with new options (typically x/y data) and optionally redraw the series. + * + * @param {Object} options Point options as defined in the series.data array + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + * + */ + update: function(options, redraw, animation) { + var point = this, + series = point.series, + dataLabel = point.dataLabel, + graphic = point.graphic, + chart = series.chart; + + redraw = pick(redraw, true); + + // fire the event with a default handler of doing the update + point.firePointEvent('update', { options: options }, function() { + + point.applyOptions(options); + + if (dataLabel) { + dataLabel.attr({ + text: point.getDataLabelText() + }) + } + + // update visuals + if (isObject(options)) { + series.getAttribs(); + if (graphic) { + graphic.attr(point.pointAttr[series.state]); + } + } + + // redraw + series.isDirty = true; + if (redraw) { + chart.redraw(animation); + } + }); + }, + + /** + * Remove a point and optionally redraw the series and if necessary the axes + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + remove: function(redraw, animation) { + var point = this, + series = point.series, + chart = series.chart, + data = series.data; + + setAnimation(animation, chart); + redraw = pick(redraw, true); + + // fire the event with a default handler of removing the point + point.firePointEvent('remove', null, function() { + + erase(data, point); + + point.destroy(); + + + // redraw + series.isDirty = true; + if (redraw) { + chart.redraw(); + } + }); + + + }, + + /** + * Fire an event on the Point object. Must not be renamed to fireEvent, as this + * causes a name clash in MooTools + * @param {String} eventType + * @param {Object} eventArgs Additional event arguments + * @param {Function} defaultFunction Default event handler + */ + firePointEvent: function(eventType, eventArgs, defaultFunction) { + var point = this, + series = this.series, + seriesOptions = series.options; + + // load event handlers on demand to save time on mouseover/out + if (seriesOptions.point.events[eventType] || ( + point.options && point.options.events && point.options.events[eventType])) { + this.importEvents(); + } + + // add default handler if in selection mode + if (eventType == 'click' && seriesOptions.allowPointSelect) { + defaultFunction = function (event) { + // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera + point.select(null, event.ctrlKey || event.metaKey || event.shiftKey); + }; + } + + fireEvent(this, eventType, eventArgs, defaultFunction); + }, + /** + * Import events from the series' and point's options. Only do it on + * demand, to save processing time on hovering. + */ + importEvents: function() { + if (!this.hasImportedEvents) { + var point = this, + options = merge(point.series.options.point, point.options), + events = options.events, + eventType; + + point.events = events; + + for (eventType in events) { + addEvent(point, eventType, events[eventType]); + } + this.hasImportedEvents = true; + + } + }, + + /** + * Set the point's state + * @param {String} state + */ + setState: function(state) { + var point = this, + series = point.series, + stateOptions = series.options.states, + markerOptions = defaultPlotOptions[series.type].marker && series.options.marker, + normalDisabled = markerOptions && !markerOptions.enabled, + markerStateOptions = markerOptions && markerOptions.states[state], + stateDisabled = markerStateOptions && markerStateOptions.enabled === false, + stateMarkerGraphic = series.stateMarkerGraphic, + chart = series.chart, + pointAttr = point.pointAttr; + + if (!state) { + state = NORMAL_STATE; // empty string + } + + if ( + // already has this state + state == point.state || + // selected points don't respond to hover + (point.selected && state != SELECT_STATE) || + // series' state options is disabled + (stateOptions[state] && stateOptions[state].enabled === false) || + // point marker's state options is disabled + (state && (stateDisabled || normalDisabled && !markerStateOptions.enabled)) + + ) { + return; + } + + // apply hover styles to the existing point + if (point.graphic) { + point.graphic.attr(pointAttr[state]); + } + // if a graphic is not applied to each point in the normal state, create a shared + // graphic for the hover state + else { + if (state) { + if (!stateMarkerGraphic) { + series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.circle( + 0, 0, pointAttr[state].r + ) + .attr(pointAttr[state]) + .add(series.group); + } + + stateMarkerGraphic.translate( + point.plotX, + point.plotY + ); + } + + if (stateMarkerGraphic) { + stateMarkerGraphic[state ? 'show' : 'hide'](); + } + } + + point.state = state; + } +}; + +/** + * The base function which all other series types inherit from + * @param {Object} chart + * @param {Object} options + */ +var Series = function() {}; + +Series.prototype = { + + isCartesian: true, + type: 'line', + pointClass: Point, + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'lineColor', + 'stroke-width': 'lineWidth', + fill: 'fillColor', + r: 'radius' + }, + init: function(chart, options) { + var series = this, + eventType, + events, + //pointEvent, + index = chart.series.length; + + series.chart = chart; + options = series.setOptions(options); // merge with plotOptions + + // set some variables + extend(series, { + index: index, + options: options, + name: options.name || 'Series '+ (index + 1), + state: NORMAL_STATE, + pointAttr: {}, + visible: options.visible !== false, // true by default + selected: options.selected === true // false by default + }); + + // register event listeners + events = options.events; + for (eventType in events) { + addEvent(series, eventType, events[eventType]); + } + if ( + (events && events.click) || + (options.point && options.point.events && options.point.events.click) || + options.allowPointSelect + ) { + chart.runTrackerClick = true; + } + + series.getColor(); + series.getSymbol(); + + // set the data + series.setData(options.data, false); + + }, + + + /** + * Return an auto incremented x value based on the pointStart and pointInterval options. + * This is only used if an x value is not given for the point that calls autoIncrement. + */ + autoIncrement: function() { + var series = this, + options = series.options, + xIncrement = series.xIncrement; + + xIncrement = pick(xIncrement, options.pointStart, 0); + + series.pointInterval = pick(series.pointInterval, options.pointInterval, 1); + + series.xIncrement = xIncrement + series.pointInterval; + return xIncrement; + }, + + /** + * Sort the data and remove duplicates + */ + cleanData: function() { + var series = this, + chart = series.chart, + data = series.data, + closestPoints, + smallestInterval, + chartSmallestInterval = chart.smallestInterval, + interval, + i; + + // sort the data points + data.sort(function(a, b){ + return (a.x - b.x); + }); + + // remove points with equal x values + // record the closest distance for calculation of column widths + for (i = data.length - 1; i >= 0; i--) { + if (data[i - 1]) { + if (data[i - 1].x == data[i].x) { + data.splice(i - 1, 1); // remove the duplicate + } + + } + } + + + // find the closes pair of points + for (i = data.length - 1; i >= 0; i--) { + if (data[i - 1]) { + interval = data[i].x - data[i - 1].x; + if (smallestInterval === UNDEFINED || interval < smallestInterval) { + smallestInterval = interval; + closestPoints = i; + } + } + } + + if (chartSmallestInterval === UNDEFINED || smallestInterval < chartSmallestInterval) { + chart.smallestInterval = smallestInterval; + } + series.closestPoints = closestPoints; + }, + + /** + * Divide the series data into segments divided by null values. Also sort + * the data points and delete duplicate values. + */ + getSegments: function() { + var lastNull = -1, + segments = [], + data = this.data; + + // create the segments + each(data, function(point, i) { + if (point.y === null) { + if (i > lastNull + 1) { + segments.push(data.slice(lastNull + 1, i)); + } + lastNull = i; + } else if (i == data.length - 1) { // last value + segments.push(data.slice(lastNull + 1, i + 1)); + } + }); + this.segments = segments; + + + }, + /** + * Set the series options by merging from the options tree + * @param {Object} itemOptions + */ + setOptions: function(itemOptions) { + var plotOptions = this.chart.options.plotOptions, + options = merge( + plotOptions[this.type], + plotOptions.series, + itemOptions + ); + + return options; + + }, + /** + * Get the series' color + */ + getColor: function(){ + var defaultColors = this.chart.options.colors; + this.color = this.options.color || defaultColors[colorCounter++] || '#0000ff'; + if (colorCounter >= defaultColors.length) { + colorCounter = 0; + } + }, + /** + * Get the series' symbol + */ + getSymbol: function(){ + var defaultSymbols = this.chart.options.symbols, + symbol = this.options.marker.symbol || defaultSymbols[symbolCounter++]; + this.symbol = symbol; + if (symbolCounter >= defaultSymbols.length) { + symbolCounter = 0; + } + }, + + /** + * Add a point dynamically after chart load time + * @param {Object} options Point options as given in series.data + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean} shift If shift is true, a point is shifted off the start + * of the series as one is appended to the end. + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + addPoint: function(options, redraw, shift, animation) { + var series = this, + data = series.data, + graph = series.graph, + area = series.area, + chart = series.chart, + point = (new series.pointClass()).init(series, options); + + setAnimation(animation, chart); + + if (graph && shift) { // make graph animate sideways + graph.shift = shift; + } + if (area) { + area.shift = shift; + area.isArea = true; + } + + redraw = pick(redraw, true); + + data.push(point); + if (shift) { + data[0].remove(false); + } + + + // redraw + series.isDirty = true; + if (redraw) { + chart.redraw(); + } + }, + + /** + * Replace the series data with a new set of data + * @param {Object} data + * @param {Object} redraw + */ + setData: function(data, redraw) { + var series = this, + oldData = series.data, + initialColor = series.initialColor, + chart = series.chart, + i = oldData && oldData.length || 0; + + series.xIncrement = null; // reset for new data + if (defined(initialColor)) { // reset colors for pie + colorCounter = initialColor; + } + + data = map(splat(data || []), function(pointOptions) { + return (new series.pointClass()).init(series, pointOptions); + }); + + // destroy old points + while (i--) { + oldData[i].destroy(); + } + + // set the data + series.data = data; + + series.cleanData(); + series.getSegments(); + + // redraw + series.isDirty = true; + chart.isDirtyBox = true; + if (pick(redraw, true)) { + chart.redraw(false); + } + }, + + /** + * Remove a series and optionally redraw the chart + * + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + + remove: function(redraw, animation) { + var series = this, + chart = series.chart; + redraw = pick(redraw, true); + + if (!series.isRemoving) { /* prevent triggering native event in jQuery + (calling the remove function from the remove event) */ + series.isRemoving = true; + + // fire the event with a default handler of removing the point + fireEvent(series, 'remove', null, function() { + + + // destroy elements + series.destroy(); + + + // redraw + chart.isDirtyLegend = chart.isDirtyBox = true; + if (redraw) { + chart.redraw(animation); + } + }); + + } + series.isRemoving = false; + }, + + /** + * Translate data points from raw data values to chart specific positioning data + * needed later in drawPoints, drawGraph and drawTracker. + */ + translate: function() { + var series = this, + chart = series.chart, + stacking = series.options.stacking, + categories = series.xAxis.categories, + yAxis = series.yAxis, + data = series.data, + i = data.length; + + // do the translation + while (i--) { + var point = data[i], + xValue = point.x, + yValue = point.y, + yBottom = point.low, + stack = yAxis.stacks[(yValue < 0 ? '-' : '') + series.stackKey], + pointStack, + pointStackTotal; + point.plotX = series.xAxis.translate(xValue); + + // calculate the bottom y value for stacked series + if (stacking && series.visible && stack && stack[xValue]) { + pointStack = stack[xValue]; + pointStackTotal = pointStack.total; + pointStack.cum = yBottom = pointStack.cum - yValue; // start from top + yValue = yBottom + yValue; + + if (stacking == 'percent') { + yBottom = pointStackTotal ? yBottom * 100 / pointStackTotal : 0; + yValue = pointStackTotal ? yValue * 100 / pointStackTotal : 0; + } + + point.percentage = pointStackTotal ? point.y * 100 / pointStackTotal : 0; + point.stackTotal = pointStackTotal; + } + + if (defined(yBottom)) { + point.yBottom = yAxis.translate(yBottom, 0, 1); + } + + // set the y value + if (yValue !== null) { + point.plotY = yAxis.translate(yValue, 0, 1); + } + + // set client related positions for mouse tracking + point.clientX = chart.inverted ? + chart.plotHeight - point.plotX : + point.plotX; // for mouse tracking + + // some API data + point.category = categories && categories[point.x] !== UNDEFINED ? + categories[point.x] : point.x; + + } + }, + /** + * Memoize tooltip texts and positions + */ + setTooltipPoints: function (renew) { + var series = this, + chart = series.chart, + inverted = chart.inverted, + data = [], + plotSize = mathRound((inverted ? chart.plotTop : chart.plotLeft) + chart.plotSizeX), + low, + high, + tooltipPoints = []; // a lookup array for each pixel in the x dimension + + // renew + if (renew) { + series.tooltipPoints = null; + } + + // concat segments to overcome null values + each(series.segments, function(segment){ + data = data.concat(segment); + }); + + // loop the concatenated data and apply each point to all the closest + // pixel positions + if (series.xAxis && series.xAxis.reversed) { + data = data.reverse();//reverseArray(data); + } + + each(data, function(point, i) { + + low = data[i - 1] ? data[i - 1].high + 1 : 0; + high = point.high = data[i + 1] ? ( + mathFloor((point.plotX + (data[i + 1] ? + data[i + 1].plotX : plotSize)) / 2)) : + plotSize; + + while (low <= high) { + tooltipPoints[inverted ? plotSize - low++ : low++] = point; + } + }); + series.tooltipPoints = tooltipPoints; + }, + + + + + /** + * Series mouse over handler + */ + onMouseOver: function() { + var series = this, + chart = series.chart, + hoverSeries = chart.hoverSeries; + + if (!hasTouch && chart.mouseIsDown) { + return; + } + + // set normal state to previous series + if (hoverSeries && hoverSeries != series) { + hoverSeries.onMouseOut(); + } + + // trigger the event, but to save processing time, + // only if defined + if (series.options.events.mouseOver) { + fireEvent(series, 'mouseOver'); + } + + + // bring to front + // Todo: optimize. This is one of two operations slowing down the tooltip in Firefox. + // Can the tracking be done otherwise? + if (series.tracker) { + series.tracker.toFront(); + } + + // hover this + series.setState(HOVER_STATE); + chart.hoverSeries = series; + }, + + /** + * Series mouse out handler + */ + onMouseOut: function() { + // trigger the event only if listeners exist + var series = this, + options = series.options, + chart = series.chart, + tooltip = chart.tooltip, + hoverPoint = chart.hoverPoint; + + // trigger mouse out on the point, which must be in this series + if (hoverPoint) { + hoverPoint.onMouseOut(); + } + + // fire the mouse out event + if (series && options.events.mouseOut) { + fireEvent(series, 'mouseOut'); + } + + + // hide the tooltip + if (tooltip && !options.stickyTracking) { + tooltip.hide(); + } + + // set normal state + series.setState(); + chart.hoverSeries = null; + }, + + /** + * Animate in the series + */ + animate: function(init) { + var series = this, + chart = series.chart, + clipRect = series.clipRect, + animation = series.options.animation; + + if (animation && !isObject(animation)) { + animation = {}; + } + + if (init) { // initialize the animation + if (!clipRect.isAnimating) { // apply it only for one of the series + clipRect.attr( 'width', 0 ); + clipRect.isAnimating = true; + } + + } else { // run the animation + clipRect.animate({ + width: chart.plotSizeX + }, animation); + + // delete this function to allow it only once + this.animate = null; + } + }, + + + /** + * Draw the markers + */ + drawPoints: function(){ + var series = this, + pointAttr, + data = series.data, + chart = series.chart, + plotX, + plotY, + i, + point, + radius, + graphic; + + if (series.options.marker.enabled) { + i = data.length; + while (i--) { + point = data[i]; + plotX = point.plotX; + plotY = point.plotY; + graphic = point.graphic; + + // only draw the point if y is defined + if (plotY !== UNDEFINED && !isNaN(plotY)) { + + /* && removed this code because points stayed after zoom + point.plotX >= 0 && point.plotX <= chart.plotSizeX && + point.plotY >= 0 && point.plotY <= chart.plotSizeY*/ + + // shortcuts + pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]; + radius = pointAttr.r; + + if (graphic) { // update + graphic.animate({ + x: plotX, + y: plotY, + r: radius + }); + } else { + point.graphic = chart.renderer.symbol( + pick(point.marker && point.marker.symbol, series.symbol), + plotX, + plotY, + radius + ) + .attr(pointAttr) + .add(series.group); + } + } + } + } + + }, + + /** + * Convert state properties from API naming conventions to SVG attributes + * + * @param {Object} options API options object + * @param {Object} base1 SVG attribute object to inherit from + * @param {Object} base2 Second level SVG attribute object to inherit from + */ + convertAttribs: function(options, base1, base2, base3) { + var conversion = this.pointAttrToOptions, + attr, + option, + obj = {}; + + options = options || {}; + base1 = base1 || {}; + base2 = base2 || {}; + base3 = base3 || {}; + + for (attr in conversion) { + option = conversion[attr]; + obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]); + } + return obj; + }, + + /** + * Get the state attributes. Each series type has its own set of attributes + * that are allowed to change on a point's state change. Series wide attributes are stored for + * all series, and additionally point specific attributes are stored for all + * points with individual marker options. If such options are not defined for the point, + * a reference to the series wide attributes is stored in point.pointAttr. + */ + getAttribs: function() { + var series = this, + normalOptions = defaultPlotOptions[series.type].marker ? series.options.marker : series.options, + stateOptions = normalOptions.states, + stateOptionsHover = stateOptions[HOVER_STATE], + pointStateOptionsHover, + seriesColor = series.color, + normalDefaults = { + stroke: seriesColor, + fill: seriesColor + }, + data = series.data, + i, + point, + seriesPointAttr = [], + pointAttr, + pointAttrToOptions = series.pointAttrToOptions, + hasPointSpecificOptions; + + // series type specific modifications + if (series.options.marker) { // line, spline, area, areaspline, scatter + + // if no hover radius is given, default to normal radius + 2 + stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + 2; + stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + 1; + + } else { // column, bar, pie + + // if no hover color is given, brighten the normal color + stateOptionsHover.color = stateOptionsHover.color || + Color(stateOptionsHover.color || seriesColor) + .brighten(stateOptionsHover.brightness).get(); + } + + // general point attributes for the series normal state + seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults); + + // HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius + each([HOVER_STATE, SELECT_STATE], function(state) { + seriesPointAttr[state] = + series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]); + }); + + // set it + series.pointAttr = seriesPointAttr; + + + // Generate the point-specific attribute collections if specific point + // options are given. If not, create a referance to the series wide point + // attributes + i = data.length; + while (i--) { + point = data[i]; + normalOptions = (point.options && point.options.marker) || point.options; + if (normalOptions && normalOptions.enabled === false) { + normalOptions.radius = 0; + } + hasPointSpecificOptions = false; + + // check if the point has specific visual options + if (point.options) { + for (var key in pointAttrToOptions) { + if (defined(normalOptions[pointAttrToOptions[key]])) { + hasPointSpecificOptions = true; + } + } + } + + + + // a specific marker config object is defined for the individual point: + // create it's own attribute collection + if (hasPointSpecificOptions) { + + pointAttr = []; + stateOptions = normalOptions.states || {}; // reassign for individual point + pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {}; + + // if no hover color is given, brighten the normal color + if (!series.options.marker) { // column, bar, point + pointStateOptionsHover.color = + Color(pointStateOptionsHover.color || point.options.color) + .brighten(pointStateOptionsHover.brightness || + stateOptionsHover.brightness).get(); + + } + + // normal point state inherits series wide normal state + pointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, seriesPointAttr[NORMAL_STATE]); + + // inherit from point normal and series hover + pointAttr[HOVER_STATE] = series.convertAttribs( + stateOptions[HOVER_STATE], + seriesPointAttr[HOVER_STATE], + pointAttr[NORMAL_STATE] + ); + // inherit from point normal and series hover + pointAttr[SELECT_STATE] = series.convertAttribs( + stateOptions[SELECT_STATE], + seriesPointAttr[SELECT_STATE], + pointAttr[NORMAL_STATE] + ); + + + + // no marker config object is created: copy a reference to the series-wide + // attribute collection + } else { + pointAttr = seriesPointAttr; + } + + point.pointAttr = pointAttr; + + } + + }, + + + /** + * Clear DOM objects and free up memory + */ + destroy: function() { + var series = this, + chart = series.chart, + //chartSeries = series.chart.series, + clipRect = series.clipRect, + issue134 = /\/5[0-9\.]+ (Safari|Mobile)\//.test(userAgent), // todo: update when Safari bug is fixed + destroy, + prop; + + // remove all events + removeEvent(series); + + // remove legend items + if (series.legendItem) { + series.chart.legend.destroyItem(series); + } + + // destroy all points with their elements + each(series.data, function(point) { + point.destroy(); + }); + // destroy all SVGElements associated to the series + each(['area', 'graph', 'dataLabelsGroup', 'group', 'tracker'], function(prop) { + if (series[prop]) { + + // issue 134 workaround + destroy = issue134 && prop == 'group' ? + 'hide' : + 'destroy'; + + series[prop][destroy](); + } + }); + + // remove from hoverSeries + if (chart.hoverSeries == series) { + chart.hoverSeries = null; + } + erase(chart.series, series); + + // clear all members + for (prop in series) { + delete series[prop]; + } + }, + + /** + * Draw the data labels + */ + drawDataLabels: function() { + if (this.options.dataLabels.enabled) { + var series = this, + x, + y, + data = series.data, + options = series.options.dataLabels, + str, + dataLabelsGroup = series.dataLabelsGroup, + chart = series.chart, + inverted = chart.inverted, + seriesType = series.type, + color; + + // create a separate group for the data labels to avoid rotation + if (!dataLabelsGroup) { + dataLabelsGroup = series.dataLabelsGroup = + chart.renderer.g(PREFIX +'data-labels') + .attr({ + visibility: series.visible ? VISIBLE : HIDDEN, + zIndex: 5 + }) + .translate(chart.plotLeft, chart.plotTop) + .add(); + } + + // determine the color + color = options.color; + if (color == 'auto') { // 1.0 backwards compatibility + color = null; + } + options.style.color = pick(color, series.color); + + // make the labels for each point + each(data, function(point, i){ + var barX = point.barX, + plotX = barX && barX + point.barW / 2 || point.plotX || -999, + plotY = pick(point.plotY, -999), + dataLabel = point.dataLabel, + align = options.align; + + // get the string + str = point.getDataLabelText(); + x = (inverted ? chart.plotWidth - plotY : plotX) + options.x; + y = (inverted ? chart.plotHeight - plotX : plotY) + options.y; + + // in columns, align the string to the column + if (seriesType == 'column') { + x += { left: -1, right: 1 }[align] * point.barW / 2 || 0; + } + + + if (dataLabel) { + dataLabel.animate({ + x: x, + y: y + }); + } else if (defined(str)) { + dataLabel = point.dataLabel = chart.renderer.text( + str, + x, + y + ) + .attr({ + align: align, + rotation: options.rotation, + zIndex: 1 + }) + .css(options.style) + .add(dataLabelsGroup); + } + + // vertically centered + if (inverted && !options.y) { + dataLabel.attr({ + y: y + parseInt(dataLabel.styles.lineHeight) * 0.9 - dataLabel.getBBox().height / 2 + }); + } + + /*if (series.isCartesian) { + dataLabel[chart.isInsidePlot(plotX, plotY) ? 'show' : 'hide'](); + }*/ + + }); + } + }, + + /** + * Draw the actual graph + */ + drawGraph: function(state) { + var series = this, + options = series.options, + chart = series.chart, + graph = series.graph, + graphPath = [], + fillColor, + area = series.area, + group = series.group, + color = options.lineColor || series.color, + lineWidth = options.lineWidth, + dashStyle = options.dashStyle, + segmentPath, + renderer = chart.renderer, + translatedThreshold = series.yAxis.getThreshold(options.threshold || 0), + useArea = /^area/.test(series.type), + singlePoints = [], // used in drawTracker + areaPath = [], + attribs; + + + // divide into segments and build graph and area paths + each(series.segments, function(segment) { + segmentPath = []; + + // build the segment line + each(segment, function(point, i) { + + if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object + segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i)); + + } else { + + // moveTo or lineTo + segmentPath.push(i ? L : M); + + // step line? + if (i && options.step) { + var lastPoint = segment[i - 1]; + segmentPath.push( + point.plotX, + lastPoint.plotY + ); + } + + // normal line to next point + segmentPath.push( + point.plotX, + point.plotY + ); + } + }); + + // add the segment to the graph, or a single point for tracking + if (segment.length > 1) { + graphPath = graphPath.concat(segmentPath); + } else { + singlePoints.push(segment[0]); + } + + // build the area + if (useArea) { + var areaSegmentPath = [], + i, + segLength = segmentPath.length; + for (i = 0; i < segLength; i++) { + areaSegmentPath.push(segmentPath[i]); + } + if (segLength == 3) { // for animation from 1 to two points + areaSegmentPath.push(L, segmentPath[1], segmentPath[2]); + } + if (options.stacking && series.type != 'areaspline') { + // follow stack back. Todo: implement areaspline + for (i = segment.length - 1; i >= 0; i--) { + areaSegmentPath.push(segment[i].plotX, segment[i].yBottom); + } + + } else { // follow zero line back + areaSegmentPath.push( + L, + segment[segment.length - 1].plotX, + translatedThreshold, + L, + segment[0].plotX, + translatedThreshold + ); + } + areaPath = areaPath.concat(areaSegmentPath); + } + }); + + // used in drawTracker: + series.graphPath = graphPath; + series.singlePoints = singlePoints; + + // draw the area if area series or areaspline + if (useArea) { + fillColor = pick( + options.fillColor, + Color(series.color).setOpacity(options.fillOpacity || 0.75).get() + ); + if (area) { + area.animate({ d: areaPath }); + + } else { + // draw the area + series.area = series.chart.renderer.path(areaPath) + .attr({ + fill: fillColor + }).add(group); + } + } + + // draw the graph + if (graph) { + //graph.animate({ d: graphPath.join(' ') }); + graph.animate({ d: graphPath }); + + } else { + if (lineWidth) { + attribs = { + 'stroke': color, + 'stroke-width': lineWidth + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + + series.graph = renderer.path(graphPath) + .attr(attribs).add(group).shadow(options.shadow); + } + } + }, + + + /** + * Render the graph and markers + */ + render: function() { + var series = this, + chart = series.chart, + group, + setInvert, + options = series.options, + animation = options.animation, + doAnimation = animation && series.animate, + duration = doAnimation ? animation && animation.duration || 500 : 0, + clipRect = series.clipRect, + renderer = chart.renderer; + + + // Add plot area clipping rectangle. If this is before chart.hasRendered, + // create one shared clipRect. + if (!clipRect) { + clipRect = series.clipRect = !chart.hasRendered && chart.clipRect ? + chart.clipRect : + renderer.clipRect(0, 0, chart.plotSizeX, chart.plotSizeY); + if (!chart.clipRect) { + chart.clipRect = clipRect; + } + } + + + // the group + if (!series.group) { + group = series.group = renderer.g('series'); + + if (chart.inverted) { + setInvert = function() { + group.attr({ + width: chart.plotWidth, + height: chart.plotHeight + }).invert(); + }; + + setInvert(); // do it now + addEvent(chart, 'resize', setInvert); // do it on resize + } + group.clip(series.clipRect) + .attr({ + visibility: series.visible ? VISIBLE : HIDDEN, + zIndex: options.zIndex + }) + .translate(chart.plotLeft, chart.plotTop) + .add(chart.seriesGroup); + } + + series.drawDataLabels(); + + // initiate the animation + if (doAnimation) { + series.animate(true); + } + + // cache attributes for shapes + series.getAttribs(); + + // draw the graph if any + if (series.drawGraph) { + series.drawGraph(); + } + + // draw the points + series.drawPoints(); + + // draw the mouse tracking area + if (series.options.enableMouseTracking !== false) { + series.drawTracker(); + } + + // run the animation + if (doAnimation) { + series.animate(); + } + + // finish the individual clipRect + setTimeout(function() { + clipRect.isAnimating = false; + group = series.group; // can be destroyed during the timeout + if (group && clipRect != chart.clipRect && clipRect.renderer) { + group.clip((series.clipRect = chart.clipRect)); + clipRect.destroy(); + } + }, duration); + + + series.isDirty = false; // means data is in accordance with what you see + + }, + + /** + * Redraw the series after an update in the axes. + */ + redraw: function() { + var series = this, + chart = series.chart, + clipRect = series.clipRect, + group = series.group; + + /*if (clipRect) { + stop(clipRect); + clipRect.animate({ // for chart resize + width: chart.plotSizeX, + height: chart.plotSizeY + }); + }*/ + + // reposition on resize + if (group) { + if (chart.inverted) { + group.attr({ + width: chart.plotWidth, + height: chart.plotHeight + }); + } + + group.animate({ + translateX: chart.plotLeft, + translateY: chart.plotTop + }); + } + + series.translate(); + series.setTooltipPoints(true); + series.render(); + }, + + /** + * Set the state of the graph + */ + setState: function(state) { + var series = this, + options = series.options, + graph = series.graph, + stateOptions = options.states, + lineWidth = options.lineWidth; + + state = state || NORMAL_STATE; + + if (series.state != state) { + series.state = state; + + if (stateOptions[state] && stateOptions[state].enabled === false) { + return; + } + + if (state) { + lineWidth = stateOptions[state].lineWidth || lineWidth + 1; + } + + if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML + graph.attr({ // use attr because animate will cause any other animation on the graph to stop + 'stroke-width': lineWidth + }, state ? 0 : 500); + } + } + }, + + /** + * Set the visibility of the graph + * + * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED, + * the visibility is toggled. + */ + setVisible: function(vis, redraw) { + var series = this, + chart = series.chart, + legendItem = series.legendItem, + seriesGroup = series.group, + seriesTracker = series.tracker, + dataLabelsGroup = series.dataLabelsGroup, + showOrHide, + i, + data = series.data, + point, + ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries, + oldVisibility = series.visible; + + // if called without an argument, toggle visibility + series.visible = vis = vis === UNDEFINED ? !oldVisibility : vis; + showOrHide = vis ? 'show' : 'hide'; + + // show or hide series + if (seriesGroup) { // pies don't have one + seriesGroup[showOrHide](); + } + + // show or hide trackers + if (seriesTracker) { + seriesTracker[showOrHide](); + } else { + i = data.length; + while (i--) { + point = data[i]; + if (point.tracker) { + point.tracker[showOrHide](); + } + } + } + + + if (dataLabelsGroup) { + dataLabelsGroup[showOrHide](); + } + + if (legendItem) { + chart.legend.colorizeItem(series, vis); + } + + + // rescale or adapt to resized chart + series.isDirty = true; + // in a stack, all other series are affected + if (series.options.stacking) { + each(chart.series, function(otherSeries) { + if (otherSeries.options.stacking && otherSeries.visible) { + otherSeries.isDirty = true; + } + }); + } + + if (ignoreHiddenSeries) { + chart.isDirtyBox = true; + } + if (redraw !== false) { + chart.redraw(); + } + + fireEvent(series, showOrHide); + }, + + /** + * Show the graph + */ + show: function() { + this.setVisible(true); + }, + + /** + * Hide the graph + */ + hide: function() { + this.setVisible(false); + }, + + + /** + * Set the selected state of the graph + * + * @param selected {Boolean} True to select the series, false to unselect. If + * UNDEFINED, the selection state is toggled. + */ + select: function(selected) { + var series = this; + // if called without an argument, toggle + series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected; + + if (series.checkbox) { + series.checkbox.checked = selected; + } + + fireEvent(series, selected ? 'select' : 'unselect'); + }, + + + /** + * Draw the tracker object that sits above all data labels and markers to + * track mouse events on the graph or points. For the line type charts + * the tracker uses the same graphPath, but with a greater stroke width + * for better control. + */ + drawTracker: function() { + var series = this, + options = series.options, + trackerPath = [].concat(series.graphPath), + trackerPathLength = trackerPath.length, + chart = series.chart, + snap = chart.options.tooltip.snap, + tracker = series.tracker, + cursor = options.cursor, + css = cursor && { cursor: cursor }, + singlePoints = series.singlePoints, + singlePoint, + i; + + // Extend end points. A better way would be to use round linecaps, + // but those are not clickable in VML. + if (trackerPathLength) { + i = trackerPathLength + 1; + while (i--) { + if (trackerPath[i] == M) { // extend left side + trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L); + } + if ((i && trackerPath[i] == M) || i == trackerPathLength) { // extend right side + trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]); + } + } + } + + // handle single points + for (i = 0; i < singlePoints.length; i++) { + singlePoint = singlePoints[i]; + trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY, + L, singlePoint.plotX + snap, singlePoint.plotY); + } + + // draw the tracker + if (tracker) { + tracker.attr({ d: trackerPath }); + + } else { // create + series.tracker = chart.renderer.path(trackerPath) + .attr({ + isTracker: true, + stroke: TRACKER_FILL, + fill: NONE, + 'stroke-width' : options.lineWidth + 2 * snap, + visibility: series.visible ? VISIBLE : HIDDEN, + zIndex: 1 + }) + .on(hasTouch ? 'touchstart' : 'mouseover', function() { + if (chart.hoverSeries != series) { + series.onMouseOver(); + } + }) + .on('mouseout', function() { + if (!options.stickyTracking) { + series.onMouseOut(); + } + }) + .css(css) + .add(chart.trackerGroup); + } + + } + +}; // end Series prototype + + +/** + * LineSeries object + */ +var LineSeries = extendClass(Series); +seriesTypes.line = LineSeries; + +/** + * AreaSeries object + */ +var AreaSeries = extendClass(Series, { + type: 'area' +}); +seriesTypes.area = AreaSeries; + + + + +/** + * SplineSeries object + */ +var SplineSeries = extendClass( Series, { + type: 'spline', + + /** + * Draw the actual graph + */ + getPointSpline: function(segment, point, i) { + var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc + denom = smoothing + 1, + plotX = point.plotX, + plotY = point.plotY, + lastPoint = segment[i - 1], + nextPoint = segment[i + 1], + leftContX, + leftContY, + rightContX, + rightContY, + ret; + + // find control points + if (i && i < segment.length - 1) { + var lastX = lastPoint.plotX, + lastY = lastPoint.plotY, + nextX = nextPoint.plotX, + nextY = nextPoint.plotY, + correction; + + leftContX = (smoothing * plotX + lastX) / denom; + leftContY = (smoothing * plotY + lastY) / denom; + rightContX = (smoothing * plotX + nextX) / denom; + rightContY = (smoothing * plotY + nextY) / denom; + + // have the two control points make a straight line through main point + correction = ((rightContY - leftContY) * (rightContX - plotX)) / + (rightContX - leftContX) + plotY - rightContY; + + leftContY += correction; + rightContY += correction; + + // to prevent false extremes, check that control points are between + // neighbouring points' y values + if (leftContY > lastY && leftContY > plotY) { + leftContY = mathMax(lastY, plotY); + rightContY = 2 * plotY - leftContY; // mirror of left control point + } else if (leftContY < lastY && leftContY < plotY) { + leftContY = mathMin(lastY, plotY); + rightContY = 2 * plotY - leftContY; + } + if (rightContY > nextY && rightContY > plotY) { + rightContY = mathMax(nextY, plotY); + leftContY = 2 * plotY - rightContY; + } else if (rightContY < nextY && rightContY < plotY) { + rightContY = mathMin(nextY, plotY); + leftContY = 2 * plotY - rightContY; + } + + // record for drawing in next point + point.rightContX = rightContX; + point.rightContY = rightContY; + + } + + // moveTo or lineTo + if (!i) { + ret = [M, plotX, plotY]; + } + + // curve from last point to this + else { + ret = [ + 'C', + lastPoint.rightContX || lastPoint.plotX, + lastPoint.rightContY || lastPoint.plotY, + leftContX || plotX, + leftContY || plotY, + plotX, + plotY + ]; + lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later + } + return ret; + } +}); +seriesTypes.spline = SplineSeries; + + + +/** + * AreaSplineSeries object + */ +var AreaSplineSeries = extendClass(SplineSeries, { + type: 'areaspline' +}); +seriesTypes.areaspline = AreaSplineSeries; + +/** + * ColumnSeries object + */ +var ColumnSeries = extendClass(Series, { + type: 'column', + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'borderColor', + 'stroke-width': 'borderWidth', + fill: 'color', + r: 'borderRadius' + }, + init: function() { + Series.prototype.init.apply(this, arguments); + + var series = this, + chart = series.chart; + + // flag the chart in order to pad the x axis + chart.hasColumn = true; + + // if the series is added dynamically, force redraw of other + // series affected by a new column + if (chart.hasRendered) { + each(chart.series, function(otherSeries) { + if (otherSeries.type == series.type) { + otherSeries.isDirty = true; + } + }); + } + }, + + /** + * Translate each point to the plot area coordinate system and find shape positions + */ + translate: function() { + var series = this, + chart = series.chart, + columnCount = 0, + reversedXAxis = series.xAxis.reversed, + categories = series.xAxis.categories, + stackGroups = {}, + stackKey, + columnIndex; + + Series.prototype.translate.apply(series); + + // Get the total number of column type series. + // This is called on every series. Consider moving this logic to a + // chart.orderStacks() function and call it on init, addSeries and removeSeries + each(chart.series, function(otherSeries) { + if (otherSeries.type == series.type) { + if (otherSeries.options.stacking) { + stackKey = otherSeries.stackKey; + if (stackGroups[stackKey] === UNDEFINED) { + stackGroups[stackKey] = columnCount++; + } + columnIndex = stackGroups[stackKey]; + } else if (otherSeries.visible){ + columnIndex = columnCount++; + } + otherSeries.columnIndex = columnIndex; + } + }); + + // calculate the width and position of each column based on + // the number of column series in the plot, the groupPadding + // and the pointPadding options + var options = series.options, + data = series.data, + closestPoints = series.closestPoints, + categoryWidth = mathAbs( + data[1] ? data[closestPoints].plotX - data[closestPoints - 1].plotX : + chart.plotSizeX / (categories ? categories.length : 1) + ), + groupPadding = categoryWidth * options.groupPadding, + groupWidth = categoryWidth - 2 * groupPadding, + pointOffsetWidth = groupWidth / columnCount, + optionPointWidth = options.pointWidth, + pointPadding = defined(optionPointWidth) ? (pointOffsetWidth - optionPointWidth) / 2 : + pointOffsetWidth * options.pointPadding, + pointWidth = mathMax(pick(optionPointWidth, pointOffsetWidth - 2 * pointPadding), 1), + colIndex = (reversedXAxis ? columnCount - + series.columnIndex : series.columnIndex) || 0, + pointXOffset = pointPadding + (groupPadding + colIndex * + pointOffsetWidth -(categoryWidth / 2)) * + (reversedXAxis ? -1 : 1), + threshold = options.threshold || 0, + translatedThreshold = series.yAxis.getThreshold(threshold), + minPointLength = pick(options.minPointLength, 5); + + // record the new values + each(data, function(point) { + var plotY = point.plotY, + yBottom = point.yBottom || translatedThreshold, + barX = point.plotX + pointXOffset, + barY = mathCeil(mathMin(plotY, yBottom)), + barH = mathCeil(mathMax(plotY, yBottom) - barY), + trackerY; + + // handle options.minPointLength and tracker for small points + if (mathAbs(barH) < minPointLength) { + if (minPointLength) { + barH = minPointLength; + barY = + mathAbs(barY - translatedThreshold) > minPointLength ? // stacked + yBottom - minPointLength : // keep position + translatedThreshold - (plotY <= translatedThreshold ? minPointLength : 0); + } + trackerY = barY - 3; + } + + extend(point, { + barX: barX, + barY: barY, + barW: pointWidth, + barH: barH + }); + point.shapeType = 'rect'; + point.shapeArgs = { + x: barX, + y: barY, + width: pointWidth, + height: barH, + r: options.borderRadius + }; + + // make small columns responsive to mouse + point.trackerArgs = defined(trackerY) && merge(point.shapeArgs, { + height: mathMax(6, barH + 3), + y: trackerY + }); + }); + + }, + + getSymbol: function(){ + }, + + /** + * Columns have no graph + */ + drawGraph: function() {}, + + /** + * Draw the columns. For bars, the series.group is rotated, so the same coordinates + * apply for columns and bars. This method is inherited by scatter series. + * + */ + drawPoints: function() { + var series = this, + options = series.options, + renderer = series.chart.renderer, + graphic, + shapeArgs; + + + // draw the columns + each(series.data, function(point) { + var plotY = point.plotY; + if (plotY !== UNDEFINED && !isNaN(plotY)) { + graphic = point.graphic; + shapeArgs = point.shapeArgs; + if (graphic) { // update + stop(graphic); + graphic.animate(shapeArgs); + + } else { + point.graphic = renderer[point.shapeType](shapeArgs) + .attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]) + .add(series.group) + .shadow(options.shadow); + } + + } + }); + }, + /** + * Draw the individual tracker elements. + * This method is inherited by scatter and pie charts too. + */ + drawTracker: function() { + var series = this, + chart = series.chart, + renderer = chart.renderer, + shapeArgs, + tracker, + trackerLabel = +new Date(), + cursor = series.options.cursor, + css = cursor && { cursor: cursor }, + rel; + + each(series.data, function(point) { + tracker = point.tracker; + shapeArgs = point.trackerArgs || point.shapeArgs; + if (point.y !== null) { + if (tracker) {// update + tracker.attr(shapeArgs); + + } else { + point.tracker = + renderer[point.shapeType](shapeArgs) + .attr({ + isTracker: trackerLabel, + fill: TRACKER_FILL, + visibility: series.visible ? VISIBLE : HIDDEN, + zIndex: 1 + }) + .on(hasTouch ? 'touchstart' : 'mouseover', function(event) { + rel = event.relatedTarget || event.fromElement; + if (chart.hoverSeries != series && attr(rel, 'isTracker') != trackerLabel) { + series.onMouseOver(); + } + point.onMouseOver(); + + }) + .on('mouseout', function(event) { + if (!series.options.stickyTracking) { + rel = event.relatedTarget || event.toElement; + if (attr(rel, 'isTracker') != trackerLabel) { + series.onMouseOut(); + } + } + }) + .css(css) + .add(chart.trackerGroup); + } + } + }); + }, + + + /** + * Animate the column heights one by one from zero + * @param {Boolean} init Whether to initialize the animation or run it + */ + animate: function(init) { + var series = this, + data = series.data; + + if (!init) { // run the animation + /* + * Note: Ideally the animation should be initialized by calling + * series.group.hide(), and then calling series.group.show() + * after the animation was started. But this rendered the shadows + * invisible in IE8 standards mode. If the columns flicker on large + * datasets, this is the cause. + */ + + each(data, function(point) { + var graphic = point.graphic; + + if (graphic) { + // start values + graphic.attr({ + height: 0, + y: series.yAxis.translate(0, 0, 1) + }); + + // animate + graphic.animate({ + height: point.barH, + y: point.barY + }, series.options.animation); + } + }); + + + // delete this function to allow it only once + series.animate = null; + } + + }, + /** + * Remove this series from the chart + */ + remove: function() { + var series = this, + chart = series.chart; + + // column and bar series affects other series of the same type + // as they are either stacked or grouped + if (chart.hasRendered) { + each(chart.series, function(otherSeries) { + if (otherSeries.type == series.type) { + otherSeries.isDirty = true; + } + }); + } + + Series.prototype.remove.apply(series, arguments); + } +}); +seriesTypes.column = ColumnSeries; + +var BarSeries = extendClass(ColumnSeries, { + type: 'bar', + init: function(chart) { + chart.inverted = this.inverted = true; + ColumnSeries.prototype.init.apply(this, arguments); + } +}); +seriesTypes.bar = BarSeries; + +/** + * The scatter series class + */ +var ScatterSeries = extendClass(Series, { + type: 'scatter', + + /** + * Extend the base Series' translate method by adding shape type and + * arguments for the point trackers + */ + translate: function() { + var series = this; + + Series.prototype.translate.apply(series); + + each(series.data, function(point) { + point.shapeType = 'circle'; + point.shapeArgs = { + x: point.plotX, + y: point.plotY, + r: series.chart.options.tooltip.snap + }; + }); + }, + + + /** + * Create individual tracker elements for each point + */ + //drawTracker: ColumnSeries.prototype.drawTracker, + drawTracker: function() { + var series = this, + cursor = series.options.cursor, + css = cursor && { cursor: cursor }, + graphic; + + each(series.data, function(point) { + graphic = point.graphic; + if (graphic) { // doesn't exist for null points + graphic + .attr({ isTracker: true }) + .on('mouseover', function(event) { + series.onMouseOver(); + point.onMouseOver(); + }) + .on('mouseout', function(event) { + if (!series.options.stickyTracking) { + series.onMouseOut(); + } + }) + .css(css); + } + }); + + }, + + /** + * Cleaning the data is not necessary in a scatter plot + */ + cleanData: function() {} +}); +seriesTypes.scatter = ScatterSeries; + +/** + * Extended point object for pies + */ +var PiePoint = extendClass(Point, { + /** + * Initiate the pie slice + */ + init: function () { + + Point.prototype.init.apply(this, arguments); + + var point = this, + toggleSlice; + + //visible: options.visible !== false, + extend(point, { + visible: point.visible !== false, + name: pick(point.name, 'Slice') + }); + + // add event listener for select + toggleSlice = function() { + point.slice(); + }; + addEvent(point, 'select', toggleSlice); + addEvent(point, 'unselect', toggleSlice); + + return point; + }, + + /** + * Toggle the visibility of the pie slice + * @param {Boolean} vis Whether to show the slice or not. If undefined, the + * visibility is toggled + */ + setVisible: function(vis) { + var point = this, + chart = point.series.chart, + tracker = point.tracker, + dataLabel = point.dataLabel, + connector = point.connector, + method; + + // if called without an argument, toggle visibility + point.visible = vis = vis === UNDEFINED ? !point.visible : vis; + + method = vis ? 'show' : 'hide'; + + point.group[method](); + if (tracker) { + tracker[method](); + } + if (dataLabel) { + dataLabel[method](); + } + if (connector) { + connector[method](); + } + if (point.legendItem) { + chart.legend.colorizeItem(point, vis); + } + }, + + /** + * Set or toggle whether the slice is cut out from the pie + * @param {Boolean} sliced When undefined, the slice state is toggled + * @param {Boolean} redraw Whether to redraw the chart. True by default. + */ + slice: function(sliced, redraw, animation) { + var point = this, + series = point.series, + chart = series.chart, + slicedTranslation = point.slicedTranslation; + + setAnimation(animation, chart); + + // redraw is true by default + redraw = pick(redraw, true); + + // if called without an argument, toggle + sliced = point.sliced = defined(sliced) ? sliced : !point.sliced; + + point.group.animate({ + translateX: (sliced ? slicedTranslation[0] : chart.plotLeft), + translateY: (sliced ? slicedTranslation[1] : chart.plotTop) + }); + + } +}); + +/** + * The Pie series class + */ +var PieSeries = extendClass(Series, { + type: 'pie', + isCartesian: false, + pointClass: PiePoint, + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'borderColor', + 'stroke-width': 'borderWidth', + fill: 'color' + }, + + /** + * Pies have one color each point + */ + getColor: function() { + // record first color for use in setData + this.initialColor = colorCounter; + }, + + /** + * Animate the column heights one by one from zero + * @param {Boolean} init Whether to initialize the animation or run it + */ + animate: function(init) { + var series = this, + data = series.data; + + each(data, function(point) { + var graphic = point.graphic, + args = point.shapeArgs, + up = -mathPI / 2; + + if (graphic) { + // start values + graphic.attr({ + r: 0, + start: up, + end: up + }); + + // animate + graphic.animate({ + r: args.r, + start: args.start, + end: args.end + }, series.options.animation); + } + }); + + // delete this function to allow it only once + series.animate = null; + + }, + /** + * Do translation for pie slices + */ + translate: function() { + var total = 0, + series = this, + cumulative = -0.25, // start at top + precision = 1000, // issue #172 + options = series.options, + slicedOffset = options.slicedOffset, + connectorOffset = slicedOffset + options.borderWidth, + positions = options.center, + chart = series.chart, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + start, + end, + angle, + data = series.data, + circ = 2 * mathPI, + fraction, + smallestSize = mathMin(plotWidth, plotHeight), + isPercent, + radiusX, // the x component of the radius vector for a given point + radiusY, + labelDistance = options.dataLabels.distance; + + // get positions - either an integer or a percentage string must be given + positions.push(options.size, options.innerSize || 0); + positions = map(positions, function(length, i) { + + isPercent = /%$/.test(length); + return isPercent ? + // i == 0: centerX, relative to width + // i == 1: centerY, relative to height + // i == 2: size, relative to smallestSize + [plotWidth, plotHeight, smallestSize, smallestSize][i] * + pInt(length) / 100: + length; + }); + + // utility for getting the x value from a given y, used for anticollision logic in data labels + series.getX = function(y, left) { + + angle = math.asin((y - positions[1]) / (positions[2] / 2 + labelDistance)); + + return positions[0] + + (left ? -1 : 1) * + (mathCos(angle) * (positions[2] / 2 + labelDistance)); + }; + + // set center for later use + series.center = positions; + + // get the total sum + each(data, function(point) { + total += point.y; + }); + + each(data, function(point) { + // set start and end angle + fraction = total ? point.y / total : 0; + start = mathRound(cumulative * circ * precision) / precision; + cumulative += fraction; + end = mathRound(cumulative * circ * precision) / precision; + + // set the shape + point.shapeType = 'arc'; + point.shapeArgs = { + x: positions[0], + y: positions[1], + r: positions[2] / 2, + innerR: positions[3] / 2, + start: start, + end: end + }; + + // center for the sliced out slice + angle = (end + start) / 2; + point.slicedTranslation = map([ + mathCos(angle) * slicedOffset + chart.plotLeft, + mathSin(angle) * slicedOffset + chart.plotTop + ], mathRound); + + // set the anchor point for tooltips + radiusX = mathCos(angle) * positions[2] / 2; + radiusY = mathSin(angle) * positions[2] / 2; + point.tooltipPos = [ + positions[0] + radiusX * 0.7, + positions[1] + radiusY * 0.7 + ]; + + // set the anchor point for data labels + point.labelPos = [ + positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector + positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a + positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie + positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a + positions[0] + radiusX, // landing point for connector + positions[1] + radiusY, // a/a + labelDistance < 0 ? // alignment + 'center' : + angle < circ / 4 ? 'left' : 'right', // alignment + angle // center angle + ]; + + + // API properties + point.percentage = fraction * 100; + point.total = total; + + }); + + this.setTooltipPoints(); + }, + + /** + * Render the slices + */ + render: function() { + var series = this; + + // cache attributes for shapes + series.getAttribs(); + + this.drawPoints(); + + // draw the mouse tracking area + if (series.options.enableMouseTracking !== false) { + series.drawTracker(); + } + + this.drawDataLabels(); + + if (series.options.animation && series.animate) { + series.animate(); + } + + series.isDirty = false; // means data is in accordance with what you see + }, + + /** + * Draw the data points + */ + drawPoints: function() { + var series = this, + chart = series.chart, + renderer = chart.renderer, + groupTranslation, + //center, + graphic, + group, + shapeArgs; + + // draw the slices + each(series.data, function(point) { + graphic = point.graphic; + shapeArgs = point.shapeArgs; + group = point.group; + + // create the group the first time + if (!group) { + group = point.group = renderer.g('point') + .attr({ zIndex: 5 }) + .add(); + } + + // if the point is sliced, use special translation, else use plot area traslation + groupTranslation = point.sliced ? point.slicedTranslation : [chart.plotLeft, chart.plotTop]; + group.translate(groupTranslation[0], groupTranslation[1]) + + + // draw the slice + if (graphic) { + graphic.animate(shapeArgs); + } else { + point.graphic = + renderer.arc(shapeArgs) + .attr(extend( + point.pointAttr[NORMAL_STATE], + { 'stroke-linejoin': 'round' } + )) + .add(point.group); + } + + // detect point specific visibility + if (point.visible === false) { + point.setVisible(false); + } + + }); + + }, + + /** + * Override the base drawDataLabels method by pie specific functionality + */ + drawDataLabels: function() { + var series = this, + data = series.data, + point, + chart = series.chart, + options = series.options.dataLabels, + connectorPadding = pick(options.connectorPadding, 10), + connectorWidth = pick(options.connectorWidth, 1), + connector, + connectorPath, + outside = options.distance > 0, + dataLabel, + labelPos, + labelHeight, + lastY, + centerY = series.center[1], + quarters = [// divide the points into quarters for anti collision + [], // top right + [], // bottom right + [], // bottom left + [] // top left + ], + x, + y, + visibility, + overlapping, + rankArr, + secondPass, + sign, + lowerHalf, + sort, + i = 4, + j; + + // run parent method + Series.prototype.drawDataLabels.apply(series); + + // arrange points for detection collision + each(data, function(point) { + var angle = point.labelPos[7], + quarter; + if (angle < 0) { + quarter = 0; + } else if (angle < mathPI / 2) { + quarter = 1; + } else if (angle < mathPI) { + quarter = 2; + } else { + quarter = 3; + } + quarters[quarter].push(point); + }); + quarters[1].reverse(); + quarters[3].reverse(); + + // define the sorting algorithm + sort = function(a,b) { + return a.y > b.y; + }; + /* Loop over the points in each quartile, starting from the top and bottom + * of the pie to detect overlapping labels. + */ + while (i--) { + overlapping = 0; + + // create an array for sorting and ranking the points within each quarter + rankArr = [].concat(quarters[i]); + rankArr.sort(sort); + j = rankArr.length; + while (j--) { + rankArr[j].rank = j; + } + + /* In the first pass, count the number of overlapping labels. In the second + * pass, remove the labels with lowest rank/values. + */ + for (secondPass = 0; secondPass < 2; secondPass++) { + lowerHalf = i % 3; + lastY = lowerHalf ? 9999 : -9999; + sign = lowerHalf ? -1 : 1; + + for (j = 0; j < quarters[i].length; j++) { + point = quarters[i][j]; + + if ((dataLabel = point.dataLabel)) { + labelPos = point.labelPos; + visibility = VISIBLE; + x = labelPos[0]; + y = labelPos[1]; + + + // assume all labels have equal height + if (!labelHeight) { + labelHeight = dataLabel && dataLabel.getBBox().height; + } + + // anticollision + if (outside) { + if (secondPass && point.rank < overlapping) { + visibility = HIDDEN; + } else if ((!lowerHalf && y < lastY + labelHeight) || + (lowerHalf && y > lastY - labelHeight)) { + y = lastY + sign * labelHeight; + x = series.getX(y, i > 1); + if ((!lowerHalf && y + labelHeight > centerY) || + (lowerHalf && y -labelHeight < centerY)) { + if (secondPass) { + visibility = HIDDEN; + } else { + overlapping++; + } + } + } + } + + if (point.visible === false) { + visibility = HIDDEN; + } + + if (visibility == VISIBLE) { + lastY = y; + } + + if (secondPass) { + + // move or place the data label + dataLabel + .attr({ + visibility: visibility, + align: labelPos[6] + }) + [dataLabel.moved ? 'animate' : 'attr']({ + x: x + options.x + + ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0), + y: y + options.y + }); + dataLabel.moved = true; + + // draw the connector + if (outside && connectorWidth) { + connector = point.connector; + + connectorPath = [ + M, + x + (labelPos[6] == 'left' ? 5 : -5), y, // end of the string at the label + L, + x, y, // first break, next to the label + L, + labelPos[2], labelPos[3], // second break + L, + labelPos[4], labelPos[5] // base + ]; + + if (connector) { + connector.animate({ d: connectorPath }); + connector.attr('visibility', visibility); + + } else { + point.connector = connector = series.chart.renderer.path(connectorPath).attr({ + 'stroke-width': connectorWidth, + stroke: options.connectorColor || '#606060', + visibility: visibility, + zIndex: 3 + }) + .translate(chart.plotLeft, chart.plotTop) + .add(); + } + } + } + } + } + } + } + }, + + /** + * Draw point specific tracker objects. Inherit directly from column series. + */ + drawTracker: ColumnSeries.prototype.drawTracker, + + /** + * Pies don't have point marker symbols + */ + getSymbol: function() {} + +}); +seriesTypes.pie = PieSeries; + + +// global variables +win.Highcharts = { + Chart: Chart, + dateFormat: dateFormat, + pathAnim: pathAnim, + getOptions: getOptions, + numberFormat: numberFormat, + Point: Point, + Color: Color, + Renderer: Renderer, + seriesTypes: seriesTypes, + setOptions: setOptions, + Series: Series, + + // Expose utility funcitons for modules + addEvent: addEvent, + createElement: createElement, + discardElement: discardElement, + css: css, + each: each, + extend: extend, + map: map, + merge: merge, + pick: pick, + extendClass: extendClass, + version: '2.1.4' +}; +})(); +