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'
+};
+})();
+