diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java index 3f44b8f85d8..2731964678c 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java @@ -701,7 +701,7 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener { private Map> clusterBearingsForWindEstimation(TimePoint timePoint, DummyMarkPassingWithTimePointOnly dummyMarkPassingForNow, Weigher weigher) { - Weigher weigherForMarkPassingProximity = new LinearTimeDifferenceWeigher(getMillisecondsOverWhichToAverageSpeed()); + Weigher weigherForMarkPassingProximity = new LinearTimeDifferenceWeigher(getMillisecondsOverWhichToAverageSpeed()*5); Map> bearings = new HashMap>(); for (LegType legType : LegType.values()) { bearings.put(legType, new BearingWithConfidenceCluster(weigher)); diff --git a/java/com.sap.sailing.gwt.ui/.classpath b/java/com.sap.sailing.gwt.ui/.classpath index e200ef32b85..ce808c49c63 100755 --- a/java/com.sap.sailing.gwt.ui/.classpath +++ b/java/com.sap.sailing.gwt.ui/.classpath @@ -9,5 +9,6 @@ + diff --git a/java/com.sap.sailing.gwt.ui/AdminConsole.html b/java/com.sap.sailing.gwt.ui/AdminConsole.html old mode 100755 new mode 100644 index e6bc65a151a..5a67464c360 --- a/java/com.sap.sailing.gwt.ui/AdminConsole.html +++ b/java/com.sap.sailing.gwt.ui/AdminConsole.html @@ -18,11 +18,11 @@ SAP Sailing Analytics Administration Console - + + + + + diff --git a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF index 0d3aec7b0b9..fb63098df79 100755 --- a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF @@ -39,4 +39,5 @@ Web-ContextPath: /gwt Bundle-ClassPath: WEB-INF/classes/, WEB-INF/lib/gflot-1.0.1.jar, WEB-INF/lib/gwt-maps.jar, - WEB-INF/lib/gwt-servlet.jar + WEB-INF/lib/gwt-servlet.jar, + WEB-INF/lib/org.moxieapps.gwt.highcharts-1.1.3.jar diff --git a/java/com.sap.sailing.gwt.ui/WEB-INF/lib/Highcharts-2.1.9.zip b/java/com.sap.sailing.gwt.ui/WEB-INF/lib/Highcharts-2.1.9.zip new file mode 100755 index 00000000000..7a0c9788829 Binary files /dev/null and b/java/com.sap.sailing.gwt.ui/WEB-INF/lib/Highcharts-2.1.9.zip differ diff --git a/java/com.sap.sailing.gwt.ui/WEB-INF/lib/org.moxieapps.gwt.highcharts-1.1.3.jar b/java/com.sap.sailing.gwt.ui/WEB-INF/lib/org.moxieapps.gwt.highcharts-1.1.3.jar new file mode 100755 index 00000000000..cbf2faaf83d Binary files /dev/null and b/java/com.sap.sailing.gwt.ui/WEB-INF/lib/org.moxieapps.gwt.highcharts-1.1.3.jar differ diff --git a/java/com.sap.sailing.gwt.ui/build.properties b/java/com.sap.sailing.gwt.ui/build.properties index 3bedfb7183e..5ea57549b64 100755 --- a/java/com.sap.sailing.gwt.ui/build.properties +++ b/java/com.sap.sailing.gwt.ui/build.properties @@ -24,5 +24,7 @@ bin.includes = META-INF/,\ RaceBoard.html,\ Spectator.html,\ com.sap.sailing.gwt.ui.Spectator/,\ - com.sap.sailing.gwt.ui.RaceBoard/ + com.sap.sailing.gwt.ui.RaceBoard/,\ + WEB-INF/lib/org.moxieapps.gwt.highcharts-1.1.3.jar,\ + highcharts/ output.. = WEB-INF/classes/ diff --git a/java/com.sap.sailing.gwt.ui/highcharts/js/adapters/mootools-adapter.js b/java/com.sap.sailing.gwt.ui/highcharts/js/adapters/mootools-adapter.js new file mode 100644 index 00000000000..18cf75971d5 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/highcharts/js/adapters/mootools-adapter.js @@ -0,0 +1,12 @@ +/* + Highcharts JS v2.1.9 (2011-11-11) + MooTools adapter + + (c) 2010-2011 Torstein H?nsi + + License: www.highcharts.com/license +*/ +(function(){var e=window,h=e.MooTools.version.substring(0,3),i=h==="1.2"||h==="1.1",m=i||h==="1.3",j=e.$extend||function(){return Object.append.apply(Object,arguments)};e.HighchartsAdapter={init:function(a){var b=Fx.prototype,c=b.start,d=Fx.Morph.prototype,g=d.compute;b.start=function(f){var k=this.element;if(f.d)this.paths=a.init(k,k.d,this.toD);c.apply(this,arguments);return this};d.compute=function(f,k,n){var l=this.paths;if(l)this.element.attr("d",a.step(l[0],l[1],n,this.toD));else return g.apply(this, +arguments)}},animate:function(a,b,c){var d=a.attr,g=c&&c.complete;if(d&&!a.setStyle){a.getStyle=a.attr;a.setStyle=function(){var f=arguments;a.attr.call(a,f[0],f[1][0])};a.$family=a.uid=true}e.HighchartsAdapter.stop(a);c=new Fx.Morph(d?a:$(a),j({transition:Fx.Transitions.Quad.easeInOut},c));if(b.d)c.toD=b.d;g&&c.addEvent("complete",g);c.start(b);a.fx=c},each:function(a,b){return i?$each(a,b):a.each(b)},map:function(a,b){return a.map(b)},grep:function(a,b){return a.filter(b)},merge:function(){var a= +arguments,b=[{}],c=a.length;if(i)a=$merge.apply(null,a);else{for(;c--;)if(typeof a[c]!=="boolean")b[c+1]=a[c];a=Object.merge.apply(Object,b)}return a},extendWithEvents:function(a){a.addEvent||(a.nodeName?$(a):j(a,new Events))},addEvent:function(a,b,c){if(typeof b==="string"){if(b==="unload")b="beforeunload";e.HighchartsAdapter.extendWithEvents(a);a.addEvent(b,c)}},removeEvent:function(a,b,c){if(typeof a!=="string"){e.HighchartsAdapter.extendWithEvents(a);if(b){if(b==="unload")b="beforeunload";c?a.removeEvent(b, +c):a.removeEvents(b)}else a.removeEvents()}},fireEvent:function(a,b,c,d){b={type:b,target:a};b=m?new Event(b):new DOMEvent(b);b=j(b,c);b.preventDefault=function(){d=null};a.fireEvent&&a.fireEvent(b.type,b);d&&d(b)},stop:function(a){a.fx&&a.fx.cancel()}}})(); diff --git a/java/com.sap.sailing.gwt.ui/highcharts/js/adapters/mootools-adapter.src.js b/java/com.sap.sailing.gwt.ui/highcharts/js/adapters/mootools-adapter.src.js new file mode 100644 index 00000000000..ccd606e8811 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/highcharts/js/adapters/mootools-adapter.src.js @@ -0,0 +1,264 @@ +/** + * @license Highcharts JS v2.1.9 (2011-11-11) + * MooTools adapter + * + * (c) 2010-2011 Torstein Hønsi + * + * License: www.highcharts.com/license + */ + +// JSLint options: +/*global Fx, $, $extend, $each, $merge, Events, Event, DOMEvent */ + +(function () { + +var win = window, + mooVersion = win.MooTools.version.substring(0, 3), // Get the first three characters of the version number + legacy = mooVersion === '1.2' || mooVersion === '1.1', // 1.1 && 1.2 considered legacy, 1.3 is not. + legacyEvent = legacy || mooVersion === '1.3', // In versions 1.1 - 1.3 the event class is named Event, in newer versions it is named DOMEvent. + $extend = win.$extend || function () { + return Object.append.apply(Object, arguments); + }; + +win.HighchartsAdapter = { + /** + * Initialize the adapter. This is run once as Highcharts is first run. + * @param {Object} pathAnim The helper object to do animations across adapters. + */ + init: function (pathAnim) { + var fxProto = Fx.prototype, + fxStart = fxProto.start, + morphProto = Fx.Morph.prototype, + morphCompute = morphProto.compute; + + // override Fx.start to allow animation of SVG element wrappers + /*jslint unparam: true*//* allow unused parameters in fx functions */ + fxProto.start = function (from, to) { + var fx = this, + elem = fx.element; + + // special for animating paths + if (from.d) { + //this.fromD = this.element.d.split(' '); + fx.paths = pathAnim.init( + elem, + elem.d, + fx.toD + ); + } + fxStart.apply(fx, arguments); + + return this; // chainable + }; + + // override Fx.step to allow animation of SVG element wrappers + morphProto.compute = function (from, to, delta) { + var fx = this, + paths = fx.paths; + + if (paths) { + fx.element.attr( + 'd', + pathAnim.step(paths[0], paths[1], delta, fx.toD) + ); + } else { + return morphCompute.apply(fx, arguments); + } + }; + /*jslint unparam: false*/ + }, + + /** + * 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 isSVGElement = el.attr, + effect, + complete = options && options.complete; + + if (isSVGElement && !el.setStyle) { + // add setStyle and getStyle methods for internal use in Moo + el.getStyle = el.attr; + el.setStyle = function () { // property value is given as array in Moo - break it down + var args = arguments; + el.attr.call(el, args[0], args[1][0]); + }; + // dirty hack to trick Moo into handling el as an element wrapper + el.$family = el.uid = true; + } + + // stop running animations + win.HighchartsAdapter.stop(el); + + // define and run the effect + effect = new Fx.Morph( + isSVGElement ? el : $(el), + $extend({ + transition: Fx.Transitions.Quad.easeInOut + }, options) + ); + + // special treatment for paths + if (params.d) { + effect.toD = params.d; + } + + // jQuery-like events + if (complete) { + effect.addEvent('complete', complete); + } + + // run + effect.start(params); + + // record for use in stop method + el.fx = effect; + }, + + /** + * MooTool's each function + * + */ + each: function (arr, fn) { + return legacy ? + $each(arr, fn) : + arr.each(fn); + }, + + /** + * Map an array + * @param {Array} arr + * @param {Function} fn + */ + map: function (arr, fn) { + return arr.map(fn); + }, + + /** + * Grep or filter an array + * @param {Array} arr + * @param {Function} fn + */ + grep: function (arr, fn) { + return arr.filter(fn); + }, + + /** + * Deep merge two objects and return a third + */ + merge: function () { + var args = arguments, + args13 = [{}], // MooTools 1.3+ + i = args.length, + ret; + + if (legacy) { + ret = $merge.apply(null, args); + } else { + while (i--) { + // Boolean argumens should not be merged. + // JQuery explicitly skips this, so we do it here as well. + if (typeof args[i] !== 'boolean') { + args13[i + 1] = args[i]; + } + } + ret = Object.merge.apply(Object, args13); + } + + return ret; + }, + + /** + * Extends an object with Events, if its not done + */ + extendWithEvents: function (el) { + // if the addEvent method is not defined, el is a custom Highcharts object + // like series or point + if (!el.addEvent) { + if (el.nodeName) { + el = $(el); // a dynamically generated node + } else { + $extend(el, new Events()); // a custom object + } + } + }, + + /** + * Add an event listener + * @param {Object} el HTML element or custom object + * @param {String} type Event type + * @param {Function} fn Event handler + */ + addEvent: function (el, type, fn) { + if (typeof type === 'string') { // chart broke due to el being string, type function + + if (type === 'unload') { // Moo self destructs before custom unload events + type = 'beforeunload'; + } + + win.HighchartsAdapter.extendWithEvents(el); + + el.addEvent(type, fn); + } + }, + + removeEvent: function (el, type, fn) { + if (typeof el === 'string') { + // el.removeEvents below apperantly calls this method again. Do not quite understand why, so for now just bail out. + return; + } + win.HighchartsAdapter.extendWithEvents(el); + if (type) { + if (type === 'unload') { // Moo self destructs before custom unload events + type = 'beforeunload'; + } + + if (fn) { + el.removeEvent(type, fn); + } else { + el.removeEvents(type); + } + } else { + el.removeEvents(); + } + }, + + fireEvent: function (el, event, eventArguments, defaultFunction) { + var eventArgs = { + type: event, + target: el + }; + // create an event object that keeps all functions + event = legacyEvent ? new Event(eventArgs) : new DOMEvent(eventArgs); + event = $extend(event, eventArguments); + // override the preventDefault function to be able to use + // this for custom events + event.preventDefault = function () { + defaultFunction = null; + }; + // if fireEvent is not available on the object, there hasn't been added + // any events to it above + if (el.fireEvent) { + el.fireEvent(event.type, event); + } + + // fire the default if it is passed and it is not prevented above + if (defaultFunction) { + defaultFunction(event); + } + }, + + /** + * Stop running animations on the object + */ + stop: function (el) { + if (el.fx) { + el.fx.cancel(); + } + } +}; + +}()); diff --git a/java/com.sap.sailing.gwt.ui/highcharts/js/adapters/prototype-adapter.js b/java/com.sap.sailing.gwt.ui/highcharts/js/adapters/prototype-adapter.js new file mode 100644 index 00000000000..735851e917c --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/highcharts/js/adapters/prototype-adapter.js @@ -0,0 +1,15 @@ +/* + Highcharts JS v2.1.9 (2011-11-11) + Prototype adapter + + @author Michael Nelson, Torstein H?nsi. + + Feel free to use and modify this script. + Highcharts license: www.highcharts.com/license. +*/ +var HighchartsAdapter=function(){var g=typeof Effect!=="undefined";return{init:function(c){if(g)Effect.HighchartsTransition=Class.create(Effect.Base,{initialize:function(a,b,d,e){var f;this.element=a;this.key=b;f=a.attr(b);if(b==="d"){this.paths=c.init(a,a.d,d);this.toD=d;f=0;d=1}this.start(Object.extend(e||{},{from:f,to:d,attribute:b}))},setup:function(){HighchartsAdapter._extend(this.element);if(!this.element._highchart_animation)this.element._highchart_animation={};this.element._highchart_animation[this.key]= +this},update:function(a){var b=this.paths;if(b)a=c.step(b[0],b[1],a,this.toD);this.element.attr(this.options.attribute,a)},finish:function(){delete this.element._highchart_animation[this.key]}})},addNS:function(c){var a=/^(?:click|mouse(?:down|up|over|move|out))$/;return/^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/.test(c)||a.test(c)?c:"h:"+c},addEvent:function(c,a,b){if(c.addEventListener||c.attachEvent)Event.observe($(c),HighchartsAdapter.addNS(a),b);else{HighchartsAdapter._extend(c); +c._highcharts_observe(a,b)}},animate:function(c,a,b){var d;b=b||{};b.delay=0;b.duration=(b.duration||500)/1E3;if(g)for(d in a)new Effect.HighchartsTransition($(c),d,a[d],b);else for(d in a)c.attr(d,a[d]);if(!c.attr)throw"Todo: implement animate DOM objects";},stop:function(c){var a;if(c._highcharts_extended&&c._highchart_animation)for(a in c._highchart_animation)c._highchart_animation[a].cancel()},each:function(c,a){$A(c).each(a)},fireEvent:function(c,a,b,d){if(c.fire)c.fire(HighchartsAdapter.addNS(a), +b);else if(c._highcharts_extended){b=b||{};c._highcharts_fire(a,b)}if(b&&b.defaultPrevented)d=null;d&&d(b)},removeEvent:function(c,a,b){if($(c).stopObserving){if(a)a=HighchartsAdapter.addNS(a);$(c).stopObserving(a,b)}if(window===c)Event.stopObserving(c,a,b);else{HighchartsAdapter._extend(c);c._highcharts_stop_observing(a,b)}},grep:function(c,a){return c.findAll(a)},map:function(c,a){return c.map(a)},merge:function(){function c(a,b){var d,e;for(e in b){d=b[e];a[e]=d&&typeof d==="object"&&d.constructor!== +Array&&typeof d.nodeType!=="number"?c(a[e]||{},d):b[e]}return a}return function(){var a=arguments,b,d={};for(b=0;b {a : 'a', b : {b1 : 'b1', b2 : 'b2_prime'}, c : 'c'} + /*merge: function(){ + function doCopy(copy, original) { + var value, + key, + undef, + nil, + same, + obj, + arr, + node; + + for (key in original) { + value = original[key]; + undef = typeof(value) === 'undefined'; + nil = value === null; + same = original === copy[key]; + + if (undef || nil || same) { + continue; + } + + obj = typeof(value) === 'object'; + arr = value && obj && value.constructor == Array; + node = !!value.nodeType; + + if (obj && !arr && !node) { + copy[key] = doCopy(typeof copy[key] == 'object' ? copy[key] : {}, value); + } + else { + copy[key] = original[key]; + } + } + return copy; + } + + var args = arguments, retVal = {}; + + for (var i = 0; i < args.length; i++) { + retVal = doCopy(retVal, args[i]); + } + + return retVal; + },*/ + merge: function () { // the built-in prototype merge function doesn't do deep copy + function doCopy(copy, original) { + var value, key; + + for (key in original) { + value = original[key]; + if (value && typeof value === 'object' && value.constructor !== Array && + typeof value.nodeType !== 'number') { + copy[key] = doCopy(copy[key] || {}, value); // copy + + } else { + copy[key] = original[key]; + } + } + return copy; + } + + function merge() { + var args = arguments, + i, + retVal = {}; + + for (i = 0; i < args.length; i++) { + retVal = doCopy(retVal, args[i]); + + } + return retVal; + } + + return merge.apply(this, arguments); + }, + + // extend an object to handle highchart events (highchart objects, not svg elements). + // this is a very simple way of handling events but whatever, it works (i think) + _extend: function (object) { + if (!object._highcharts_extended) { + Object.extend(object, { + _highchart_events: {}, + _highchart_animation: null, + _highcharts_extended: true, + _highcharts_observe: function (name, fn) { + this._highchart_events[name] = [this._highchart_events[name], fn].compact().flatten(); + }, + _highcharts_stop_observing: function (name, fn) { + if (name) { + if (fn) { + this._highchart_events[name] = [this._highchart_events[name]].compact().flatten().without(fn); + } else { + delete this._highchart_events[name]; + } + } else { + this._highchart_events = {}; + } + }, + _highcharts_fire: function (name, args) { + (this._highchart_events[name] || []).each(function (fn) { + // args is never null here + if (args.stopped) { + return; // "throw $break" wasn't working. i think because of the scope of 'this'. + } + + // Attach a simple preventDefault function to skip default handler if called + args.preventDefault = function () { + args.defaultPrevented = true; + }; + + // If the event handler return false, prevent the default handler from executing + if (fn.bind(this)(args) === false) { + args.preventDefault(); + } + } +.bind(this)); + } + }); + } + } +}; +}()); diff --git a/java/com.sap.sailing.gwt.ui/highcharts/js/highcharts.js b/java/com.sap.sailing.gwt.ui/highcharts/js/highcharts.js new file mode 100644 index 00000000000..0900cd921e0 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/highcharts/js/highcharts.js @@ -0,0 +1,176 @@ +/* + Highcharts JS v2.1.9 (2011-11-11) + + (c) 2009-2011 Torstein H?nsi + + License: www.highcharts.com/license +*/ +(function(){function sa(a,b){var c;a||(a={});for(c in b)a[c]=b[c];return a}function ja(a,b){return parseInt(a,b||10)}function Sb(a){return typeof a==="string"}function Nb(a){return typeof a==="object"}function lc(a){return typeof a==="number"}function mc(a){return Fa.log(a)/Fa.LN10}function nc(a,b){for(var c=a.length;c--;)if(a[c]===b){a.splice(c,1);break}}function K(a){return a!==Wa&&a!==null}function Ga(a,b,c){var d,e;if(Sb(b))if(K(c))a.setAttribute(b,c);else{if(a&&a.getAttribute)e=a.getAttribute(b)}else if(K(b)&& +Nb(b))for(d in b)a.setAttribute(d,b[d]);return e}function zc(a){return Object.prototype.toString.call(a)==="[object Array]"?a:[a]}function A(){var a=arguments,b,c,d=a.length;for(b=0;b3?c.length%3:0;return e+(g?c.substr(0,g)+d:"")+c.substr(g).replace(/(\d{3})(?=\d)/g,"$1"+d)+(f?b+bb(a-c).toFixed(f).slice(2):"")}function Fd(a){var b={left:a.offsetLeft,top:a.offsetTop};for(a=a.offsetParent;a;){b.left+=a.offsetLeft;b.top+=a.offsetTop; +if(a!==ua.body&&a!==ua.documentElement){b.left-=a.scrollLeft;b.top-=a.scrollTop}a=a.offsetParent}return b}function Gd(){this.symbol=this.color=0}function fe(a,b,c,d,e,f,g){var h=g.x;g=g.y;var i=h-a+c-25,j=g-b+d+10,m;if(i<7)i=c+h+15;if(i+a>c+e){i-=i+a-(c+e);j-=b;m=true}if(j<5){j=5;if(m&&g>=j&&g<=j+b)j=g+b-5}else if(j+b>d+f)j=d+f-b-5;return{x:i,y:j}}function Hd(a,b){var c=a.length,d;for(d=0;dwa)wa=O;else{if(Owa)wa=S;else if(F=0){pa=0;Od=true}else if(wa<0){wa=0;Pd=true}}}})}function N(k,n){var t,r;Fb=n?1:Fa.pow(10,lb(Fa.log(k)/Fa.LN10));t=k/Fb;if(!n){n=[1,2,2.5,5,10];if(o.allowDecimals===false||ca)if(Fb===1)n=[1,2,5,10];else if(Fb<=0.1)n=[1/Fb]}for(r=0;r0||!Pd))ra+=k*Rd}Ta=ia===ra?1:Ub&&!J&&E===t.options.tickPixelInterval? +t.tickInterval:A(J,Ya?1:(ra-ia)*E/ya);if(!D&&!K(o.tickInterval))Ta=N(Ta);I.tickInterval=Ta;Wc=o.minorTickInterval==="auto"&&Ta?Ta/5:o.minorTickInterval;if(D){Ba=[];J=Xa.global.useUTC;var Q=1E3/ub,fa=6E4/ub,ka=36E5/ub;E=864E5/ub;k=6048E5/ub;r=2592E6/ub;var $=31556952E3/ub,F=[["second",Q,[1,2,5,10,15,30]],["minute",fa,[1,2,5,10,15,30]],["hour",ka,[1,2,3,4,6,8,12]],["day",E,[1,2]],["week",k,[1,2]],["month",r,[1,2,3,4,6]],["year",$,null]],O=F[6],S=O[1],aa=O[2];for(t=0;t=Q)aa.setSeconds(S>=fa?0:F*lb(aa.getSeconds()/F));if(S>=fa)aa[Jd](S>=ka?0:F*lb(aa[id]()/F));if(S>=ka)aa[Kd](S>=E?0:F*lb(aa[jd]()/F));if(S>=E)aa[ld](S>=r?1:F*lb(aa[Cc]()/F));if(S>=r){aa[Ld](S>=$?0:F*lb(aa[Rc]()/F));n=aa[Sc]()}if(S>=$){n-=n%F;aa[Md](n)}S===k&&aa[ld](aa[Cc]()-aa[kd]()+o.startOfWeek);t=1;n=aa[Sc]();Q=aa.getTime()/ub;fa=aa[Rc]();for(ka=aa[Cc]();Q< +ra&&tn&&Ba.shift();if(o.endOnTick)ra=t;else raOb[qa])Ob[qa]=Ba.length}}function Ma(){var k,n;Dc=ia;Sd=ra;V();da();Hb=fb;fb=ya/(ra-ia||1);if(!xa)for(k in s)for(n in s[k])s[k][n].cum=s[k][n].total;if(!I.isDirty)I.isDirty=ia!==Dc||ra!==Sd}function za(k){k=(new w(k)).render();Pb.push(k);return k}function eb(){var k=o.title,n=o.stackLabels,t=o.alternateGridColor,r=o.lineWidth,J,E,Q=(J=l.hasRendered)&&K(Dc)&&!isNaN(Dc);E=zb.length&&K(ia)&&K(ra);ya=R?Da:Aa;fb=ya/(ra-ia||1);cc=R?X:vb;if(E||Ub){if(Wc&&!Ya)for(E=ia+ +(Ba[0]-ia)%Wc;E<=ra;E+=Wc){Wb[E]||(Wb[E]=new q(E,true));Q&&Wb[E].isNew&&Wb[E].render(null,true);Wb[E].isActive=true;Wb[E].render()}u(Ba,function($,F){if(!Ub||$>=ia&&$<=ra){Q&&mb[$].isNew&&mb[$].render(F,true);mb[$].isActive=true;mb[$].render(F)}});t&&u(Ba,function($,F){if(F%2===0&&$=1E3?Ed(k,0):k},$c=R&&o.labels.staggerLines,ec=o.reversed,fc=Ya&&o.tickmarkPlacement==="between"?0.5:0;q.prototype={addLabel:function(){var k=this.pos, +n=o.labels,t=!(k===ia&&!A(o.showFirstLabel,1)||k===ra&&!A(o.showLastLabel,0)),r=Ya&&R&&Ya.length&&!n.step&&!n.staggerLines&&!n.rotation&&Da/Ya.length||!R&&Da/2,J=Ya&&K(Ya[k])?Ya[k]:k,E=this.label;k=je.call({isFirst:k===Ba[0],isLast:k===Ba[Ba.length-1],dateTimeLabelFormat:Xc,value:ca?Fa.pow(10,J):J});r=r&&{width:Ia(1,W(r-2*(n.padding||10)))+Ua};r=sa(r,n.style);if(E===Wa)this.label=K(k)&&t&&n.enabled?ga.text(k,0,0,n.useHTML).attr({align:n.align,rotation:n.rotation}).css(r).add(na):null;else E&&E.attr({text:k}).css(r)}, +getLabelSize:function(){var k=this.label;return k?(this.labelBBox=k.getBBox())[R?"height":"width"]:0},render:function(k,n){var t=!this.minor,r=this.label,J=this.pos,E=o.labels,Q=this.gridLine,fa=t?o.gridLineWidth:o.minorGridLineWidth,ka=t?o.gridLineColor:o.minorGridLineColor,$=t?o.gridLineDashStyle:o.minorGridLineDashStyle,F=this.mark,O=t?o.tickLength:o.minorTickLength,S=t?o.tickWidth:o.minorTickWidth||0,aa=t?o.tickColor:o.minorTickColor,Eb=t?o.tickPosition:o.minorTickPosition,kb=E.step,nb=n&&ad|| +cb,Qb;Qb=R?G(J+fc,null,null,n)+cc:X+la+(Oa?(n&&qd||$a)-Ib-X:0);nb=R?nb-vb+la-(Oa?Aa:0):nb-G(J+fc,null,null,n)-cc;if(fa){J=ha(J+fc,fa,n);if(Q===Wa){Q={stroke:ka,"stroke-width":fa};if($)Q.dashstyle=$;if(t)Q.zIndex=1;this.gridLine=Q=fa?ga.path(J).attr(Q).add(Ra):null}!n&&Q&&J&&Q.animate({d:J})}if(S){if(Eb==="inside")O=-O;if(Oa)O=-O;t=ga.crispLine([Za,Qb,nb,Ka,Qb+(R?0:-O),nb+(R?O:0)],S);if(F)F.animate({d:t});else this.mark=ga.path(t).attr({stroke:aa,"stroke-width":S}).add(na)}if(r&&!isNaN(Qb)){Qb=Qb+ +E.x-(fc&&R?fc*fb*(ec?-1:1):0);nb=nb+E.y-(fc&&!R?fc*fb*(ec?1:-1):0);K(E.y)||(nb+=ja(r.styles.lineHeight)*0.9-r.getBBox().height/2);if($c)nb+=k/(kb||1)%$c*16;if(kb)r[k%kb?"hide":"show"]();r[this.isNew?"attr":"animate"]({x:Qb,y:nb})}this.isNew=false},destroy:function(){Ac(this)}};w.prototype={render:function(){var k=this,n=k.options,t=n.label,r=k.label,J=n.width,E=n.to,Q=n.from,fa=n.value,ka,$=n.dashStyle,F=k.svgElem,O=[],S,aa,Eb=n.color;aa=n.zIndex;var kb=n.events;if(ca){Q=mc(Q);E=mc(E);fa=mc(fa)}if(J){O= +ha(fa,J);n={stroke:Eb,"stroke-width":J};if($)n.dashstyle=$}else if(K(Q)&&K(E)){Q=Ia(Q,ia);E=tb(E,ra);ka=ha(E);if((O=ha(Q))&&ka)O.push(ka[4],ka[5],ka[1],ka[2]);else O=null;n={fill:Eb}}else return;if(K(aa))n.zIndex=aa;if(F)if(O)F.animate({d:O},null,F.onGetPath);else{F.hide();F.onGetPath=function(){F.show()}}else if(O&&O.length){k.svgElem=F=ga.path(O).attr(n).add();if(kb){$=function(nb){F.on(nb,function(Qb){kb[nb].apply(k,[Qb])})};for(S in kb)$(S)}}if(t&&K(t.text)&&O&&O.length&&Da>0&&Aa>0){t=Ca({align:R&& +ka&&"center",x:R?!ka&&4:10,verticalAlign:!R&&ka&&"middle",y:R?ka?16:10:ka?6:-4,rotation:R&&!ka&&90},t);if(!r)k.label=r=ga.text(t.text,0,0).attr({align:t.textAlign||t.align,rotation:t.rotation,zIndex:aa}).css(t.style).add();ka=[O[1],O[4],A(O[6],O[1])];O=[O[2],O[5],A(O[7],O[2])];S=tb.apply(Fa,ka);aa=tb.apply(Fa,O);r.align(t,false,{x:S,y:aa,width:Ia.apply(Fa,ka)-S,height:Ia.apply(Fa,O)-aa});r.show()}else r&&r.hide();return k},destroy:function(){Ac(this);nc(Pb,this)}};x.prototype={destroy:function(){Ac(this)}, +setTotal:function(k){this.cum=this.total=k},render:function(k){var n=this.options.formatter.call(this);if(this.label)this.label.attr({text:n,visibility:ob});else this.label=l.renderer.text(n,0,0).css(this.options.style).attr({align:this.textAlign,rotation:this.options.rotation,visibility:ob}).add(k)},setOffset:function(k,n){var t=this.isNegative,r=I.translate(this.total),J=I.translate(0);J=bb(r-J);var E=l.xAxis[0].translate(this.x)+k,Q=l.plotHeight;t={x:va?t?r:r-J:E,y:va?Q-E-n:t?Q-r-J:Q-r,width:va? +J:n,height:va?n:J};this.label&&this.label.align(this.alignOptions,null,t).attr({visibility:Ab})}};G=function(k,n,t,r,J){var E=1,Q=0,fa=r?Hb:fb;r=r?Dc:ia;fa||(fa=fb);if(t){E*=-1;Q=ya}if(ec){E*=-1;Q-=E*ya}if(n){if(ec)k=ya-k;k=k/fa+r;if(ca&&J)k=Fa.pow(10,k)}else{if(ca&&J)k=mc(k);k=E*(k-r)*fa+Q}return k};ha=function(k,n,t){var r,J,E;k=G(k,null,null,t);var Q=t&&ad||cb,fa=t&&qd||$a,ka;t=J=W(k+cc);r=E=W(Q-k-cc);if(isNaN(k))ka=true;else if(R){r=ea;E=Q-vb;if(tX+Da)ka=true}else{t=X;J=fa-Ib;if(r +ea+Aa)ka=true}return ka?null:ga.crispLine([Za,t,r,Ka,J,E],n||0)};if(va&&xa&&ec===Wa)ec=true;sa(I,{addPlotBand:za,addPlotLine:za,adjustTickAmount:function(){if(Ob&&!D&&!Ya&&!Ub){var k=qc,n=Ba.length;qc=Ob[qa];if(nk)k=ia;else if(ra< +k)k=ra;return G(k,0,1)},isXAxis:xa,options:o,plotLinesAndBands:Pb,getOffset:function(){var k=zb.length&&K(ia)&&K(ra),n=0,t=0,r=o.title,J=o.labels,E=[-1,1,1,-1][L],Q;if(!na){na=ga.g("axis").attr({zIndex:7}).add();Ra=ga.g("grid").attr({zIndex:1}).add()}rc=0;if(k||Ub){u(Ba,function(fa){if(mb[fa])mb[fa].addLabel();else mb[fa]=new q(fa);if(L===0||L===2||{1:"left",3:"right"}[L]===J.align)rc=Ia(mb[fa].getLabelSize(),rc)});if($c)rc+=($c-1)*16}else for(Q in mb){mb[Q].destroy();delete mb[Q]}if(r&&r.text){if(!ba){ba= +I.axisTitle=ga.text(r.text,0,0,r.useHTML).attr({zIndex:7,rotation:r.rotation||0,align:r.textAlign||{low:"left",middle:"center",high:"right"}[r.align]}).css(r.style).add();ba.isNew=true}n=ba.getBBox()[R?"height":"width"];t=A(r.margin,R?5:10)}la=E*(o.offset||Xb[L]);nd=rc+(L!==2&&rc&&E*o.labels[R?"y":"x"])+t;Xb[L]=Ia(Xb[L],nd+n+E*la)},render:eb,setCategories:function(k,n){I.categories=p.categories=Ya=k;u(zb,function(t){t.translate();t.setTooltipPoints(true)});I.isDirty=true;A(n,true)&&l.redraw()},setExtremes:function(k, +n,t,r){t=A(t,true);Pa(I,"setExtremes",{min:k,max:n},function(){Vb=k;Gb=n;t&&l.redraw(r)})},setScale:Ma,setTickPositions:da,translate:G,redraw:function(){Yb.resetTracker&&Yb.resetTracker();eb();u(Pb,function(k){k.render()});u(zb,function(k){k.isDirty=true})},removePlotBand:ab,removePlotLine:ab,reversed:ec,stacks:s,destroy:function(){var k;pb(I);for(k in s){Ac(s[k]);s[k]=null}if(I.stackTotalGroup)I.stackTotalGroup=I.stackTotalGroup.destroy();u([mb,Wb,dc,Pb],function(n){Ac(n)});u([La,na,Ra,ba],function(n){n&& +n.destroy()});La=na=Ra=ba=null}});for(pd in B)Qa(I,pd,B[pd]);Ma()}function d(){var p={};return{add:function(q,w,x,V){if(!p[q]){w=ga.text(w,0,0).css(a.toolbar.itemStyle).align({align:"right",x:-Ib-20,y:ea+30}).on("click",V).attr({align:"right",zIndex:20}).add();p[q]=w}},remove:function(q){pc(p[q].element);p[q]=null}}}function e(p){function q(){var B=this.points||zc(this),D=B[0].series.xAxis,ca=this.x;D=D&&D.options.type==="datetime";var la=Sb(ca)||D,qa;qa=la?[''+(D?Zc("%A, %b %e, %Y", +ca):ca)+""]:[];u(B,function(ya){qa.push(ya.point.tooltipFormatter(la))});return qa.join("
")}function w(B,D){L=xa?B:(2*L+B)/3;s=xa?D:(s+D)/2;o.translate(L,s);rd=bb(B-L)>1||bb(D-s)>1?function(){w(B,D)}:null}function x(){if(!xa){var B=l.hoverPoints;o.hide();u(da,function(D){D&&D.hide()});B&&u(B,function(D){D.setState()});l.hoverPoints=null;xa=true}}var V,N=p.borderWidth,ta=p.crosshairs,da=[],Ma=p.style,za=p.shared,eb=ja(Ma.padding),ab=N+eb,xa=true,Oa,R,L=0,s=0;Ma.padding=0;var o=ga.g("tooltip").attr({zIndex:8}).add(), +I=ga.rect(ab,ab,0,0,p.borderRadius,N).attr({fill:p.backgroundColor,"stroke-width":N}).add(o).shadow(p.shadow),ba=ga.text("",eb+ab,ja(Ma.fontSize)+eb+ab,p.useHTML).attr({zIndex:1}).css(Ma).add(o);o.hide();return{shared:za,refresh:function(B){var D,ca,la,qa=0,ya={},fb=[];la=B.tooltipPos;D=p.formatter||q;ya=l.hoverPoints;if(za){ya&&u(ya,function(Hb){Hb.setState()});l.hoverPoints=B;u(B,function(Hb){Hb.setState(Bb);qa+=Hb.plotY;fb.push(Hb.getLabelConfig())});ca=B[0].plotX;qa=W(qa)/B.length;ya={x:B[0].category}; +ya.points=fb;B=B[0]}else ya=B.getLabelConfig();ya=D.call(ya);V=B.series;ca=za?ca:B.plotX;qa=za?qa:B.plotY;D=W(la?la[0]:va?Da-qa:ca);ca=W(la?la[1]:va?Aa-ca:qa);la=za||!B.series.isCartesian||gc(D,ca);if(ya===false||!la)x();else{if(xa){o.show();xa=false}ba.attr({text:ya});la=ba.getBBox();Oa=la.width+2*eb;R=la.height+2*eb;I.attr({width:Oa,height:R,stroke:p.borderColor||B.color||V.color||"#606060"});D=fe(Oa,R,X,ea,Da,Aa,{x:D,y:ca});w(W(D.x-ab),W(D.y-ab))}if(ta){ta=zc(ta);for(D=ta.length;D--;){ca=B.series[D? +"yAxis":"xAxis"];if(ta[D]&&ca){ca=ca.getPlotLinePath(B[D?"y":"x"],1);if(da[D])da[D].attr({d:ca,visibility:Ab});else{la={"stroke-width":ta[D].width||1,stroke:ta[D].color||"#C0C0C0",zIndex:2};if(ta[D].dashStyle)la.dashstyle=ta[D].dashStyle;da[D]=ga.path(ca).attr(la).add()}}}}},hide:x,destroy:function(){u(da,function(B){B&&B.destroy()});u([I,ba,o],function(B){B&&B.destroy()});I=ba=o=null}}}function f(p){function q(L){var s,o=Ud&&ua.width/ua.body.scrollWidth-1,I,ba,B;L=L||db.event;if(!L.target)L.target= +L.srcElement;s=L.touches?L.touches.item(0):L;if(L.type!=="mousemove"||db.opera||o){Jb=Fd(oa);I=Jb.left;ba=Jb.top}if(Pc){B=L.x;s=L.y}else if(s.layerX===Wa){B=s.pageX-I;s=s.pageY-ba}else{B=L.layerX;s=L.layerY}if(o){B+=W((o+1)*I-I);s+=W((o+1)*ba-ba)}return sa(L,{chartX:B,chartY:s})}function w(L){var s={xAxis:[],yAxis:[]};u(Va,function(o){var I=o.translate,ba=o.isXAxis;s[ba?"xAxis":"yAxis"].push({axis:o,value:I((va?!ba:ba)?L.chartX-X:Aa-L.chartY+ea,true)})});return s}function x(){var L=l.hoverSeries, +s=l.hoverPoint;s&&s.onMouseOut();L&&L.onMouseOut();hc&&hc.hide();sd=null}function V(){if(za){var L={xAxis:[],yAxis:[]},s=za.getBBox(),o=s.x-X,I=s.y-ea;if(Ma){u(Va,function(ba){var B=ba.translate,D=ba.isXAxis,ca=va?!D:D,la=B(ca?o:Aa-I-s.height,true,0,0,1);B=B(ca?o+s.width:Aa-I,true,0,0,1);L[D?"xAxis":"yAxis"].push({axis:ba,min:tb(la,B),max:Ia(la,B)})});Pa(l,"selection",L,td)}za=za.destroy()}l.mouseIsDown=ud=Ma=false;pb(ua,Kb?"touchend":"mouseup",V)}function N(L){var s=K(L.pageX)?L.pageX:L.page.x;L= +K(L.pageX)?L.pageY:L.page.y;Jb&&!gc(s-Jb.left-X,L-Jb.top-ea)&&x()}var ta,da,Ma,za,eb=z.zoomType,ab=/x/.test(eb),xa=/y/.test(eb),Oa=ab&&!va||xa&&va,R=xa&&!va||ab&&va;bd=function(){if(Ec){Ec.translate(X,ea);va&&Ec.attr({width:l.plotWidth,height:l.plotHeight}).invert()}else l.trackerGroup=Ec=ga.g("tracker").attr({zIndex:9}).add()};bd();if(p.enabled)l.tooltip=hc=e(p);(function(){oa.onmousedown=function(s){s=q(s);!Kb&&s.preventDefault&&s.preventDefault();l.mouseIsDown=ud=true;ta=s.chartX;da=s.chartY;Qa(ua, +Kb?"touchend":"mouseup",V)};var L=function(s){if(!(s&&s.touches&&s.touches.length>1)){s=q(s);if(!Kb)s.returnValue=false;var o=s.chartX,I=s.chartY,ba=!gc(o-X,I-ea);Jb||(Jb=Fd(oa));if(Kb&&s.type==="touchstart")if(Ga(s.target,"isTracker"))l.runTrackerClick||s.preventDefault();else!ke&&!ba&&s.preventDefault();if(ba){if(oX+Da)o=X+Da;if(Iea+Aa)I=ea+Aa}if(ud&&s.type!=="touchstart"){Ma=Math.sqrt(Math.pow(ta-o,2)+Math.pow(da-I,2));if(Ma>10){if(sc&&(ab||xa)&&gc(ta-X,da-ea))za|| +(za=ga.rect(X,ea,Oa?1:Da,R?1:Aa,0).attr({fill:z.selectionMarkerFill||"rgba(69,114,167,0.25)",zIndex:7}).add());if(za&&Oa){o=o-ta;za.attr({width:bb(o),x:(o>0?0:o)+ta})}if(za&&R){I=I-da;za.attr({height:bb(I),y:(I>0?0:I)+da})}}}else if(!ba){var B;I=l.hoverPoint;o=l.hoverSeries;var D,ca,la=$a,qa=va?s.chartY:s.chartX-X;if(hc&&p.shared){B=[];D=Ha.length;for(ca=0;cala&&B.splice(D,1);if(B.length&&B[0].plotX!==sd){hc.refresh(B);sd=B[0].plotX}}if(o&&o.tracker)(s=o.tooltipPoints[qa])&&s!==I&&s.onMouseOver()}return ba||!sc}};oa.onmousemove=L;Qa(oa,"mouseleave",x);Qa(ua,"mousemove",N);oa.ontouchstart=function(s){if(ab||xa)oa.onmousedown(s);L(s)};oa.ontouchmove=L;oa.ontouchend=function(){Ma&&x()};oa.onclick=function(s){var o=l.hoverPoint;s=q(s);s.cancelBubble=true;if(!Ma)if(o&&Ga(s.target,"isTracker")){var I=o.plotX,ba=o.plotY;sa(o,{pageX:Jb.left+ +X+(va?Da-ba:I),pageY:Jb.top+ea+(va?Aa-I:ba)});Pa(o.series,"click",sa(s,{point:o}));o.firePointEvent("click",s)}else{sa(s,w(s));gc(s.chartX-X,s.chartY-ea)&&Pa(l,"click",s)}Ma=false}})();Vd=setInterval(function(){rd&&rd()},32);sa(this,{zoomX:ab,zoomY:xa,resetTracker:x,destroy:function(){if(l.trackerGroup)l.trackerGroup=Ec=l.trackerGroup.destroy();pb(ua,"mousemove",N);oa.onclick=oa.onmousedown=oa.onmousemove=oa.ontouchstart=oa.ontouchend=oa.ontouchmove=null}})}function g(p){var q=p.type||z.type||z.defaultSeriesType, +w=wb[q],x=l.hasRendered;if(x)if(va&&q==="column")w=wb.bar;else if(!va&&q==="bar")w=wb.column;q=new w;q.init(l,p);if(!x&&q.inverted)va=true;if(q.isCartesian)sc=q.isCartesian;Ha.push(q);return q}function h(){z.alignTicks!==false&&u(Va,function(p){p.adjustTickAmount()});Ob=null}function i(p){var q=l.isDirtyLegend,w,x=l.isDirtyBox,V=Ha.length,N=V,ta=l.clipRect;for(oc(p,l);N--;){p=Ha[N];if(p.isDirty&&p.options.stacking){w=true;break}}if(w)for(N=V;N--;){p=Ha[N];if(p.options.stacking)p.isDirty=true}u(Ha, +function(da){if(da.isDirty){da.cleanData();da.getSegments();if(da.options.legendType==="point")q=true}});if(q&&Fc.renderLegend){Fc.renderLegend();l.isDirtyLegend=false}if(sc){if(!cd){Ob=null;u(Va,function(da){da.setScale()})}h();Gc();u(Va,function(da){if(da.isDirty||x){da.redraw();x=true}})}if(x){vd();bd();if(ta){Hc(ta);ta.animate({width:l.plotSizeX,height:l.plotSizeY})}}u(Ha,function(da){if(da.isDirty&&da.visible&&(!da.isCartesian||da.xAxis))da.redraw()});Yb&&Yb.resetTracker&&Yb.resetTracker();Pa(l, +"redraw")}function j(){var p=a.xAxis||{},q=a.yAxis||{},w;p=zc(p);u(p,function(x,V){x.index=V;x.isX=true});q=zc(q);u(q,function(x,V){x.index=V});Va=p.concat(q);l.xAxis=[];l.yAxis=[];Va=tc(Va,function(x){w=new c(x);l[w.isXAxis?"xAxis":"yAxis"].push(w);return w});h()}function m(p,q){uc=Ca(a.title,p);Ic=Ca(a.subtitle,q);u([["title",p,uc],["subtitle",q,Ic]],function(w){var x=w[0],V=l[x],N=w[1];w=w[2];if(V&&N)V=V.destroy();if(w&&w.text&&!V)l[x]=ga.text(w.text,0,0,w.useHTML).attr({align:w.align,"class":"highcharts-"+ +x,zIndex:1}).css(w.style).add().align(w,false,Rb)})}function v(){qb=z.renderTo;Wd=vc+wd++;if(Sb(qb))qb=ua.getElementById(qb);qb.innerHTML="";if(!qb.offsetWidth){Zb=qb.cloneNode(0);Ja(Zb,{position:ic,top:"-9999px",display:""});ua.body.appendChild(Zb)}dd=(Zb||qb).offsetWidth;Jc=(Zb||qb).offsetHeight;l.chartWidth=$a=z.width||dd||600;l.chartHeight=cb=z.height||(Jc>19?Jc:400);l.container=oa=hb(Tb,{className:"highcharts-container"+(z.className?" "+z.className:""),id:Wd},sa({position:Xd,overflow:ob,width:$a+ +Ua,height:cb+Ua,textAlign:"left"},z.style),Zb||qb);l.renderer=ga=z.forExport?new ed(oa,$a,cb,true):new fd(oa,$a,cb);var p,q;if(Yd&&oa.getBoundingClientRect){p=function(){Ja(oa,{left:0,top:0});q=oa.getBoundingClientRect();Ja(oa,{left:-(q.left-ja(q.left))+Ua,top:-(q.top-ja(q.top))+Ua})};p();Qa(db,"resize",p);Qa(l,"destroy",function(){pb(db,"resize",p)})}}function P(){function p(){var w=z.width||qb.offsetWidth,x=z.height||qb.offsetHeight;if(w&&x){if(w!==dd||x!==Jc){clearTimeout(q);q=setTimeout(function(){xd(w, +x,false)},100)}dd=w;Jc=x}}var q;Qa(db,"resize",p);Qa(l,"destroy",function(){pb(db,"resize",p)})}function T(){Pa(l,"endResize",null,function(){cd-=1})}function Y(){var p=a.labels,q=a.credits,w;m();Fc=l.legend=new le;Gc();u(Va,function(x){x.setTickPositions(true)});h();Gc();vd();sc&&u(Va,function(x){x.render()});if(!l.seriesGroup)l.seriesGroup=ga.g("series-group").attr({zIndex:3}).add();u(Ha,function(x){x.translate();x.setTooltipPoints();x.render()});p.items&&u(p.items,function(){var x=sa(p.style,this.style), +V=ja(x.left)+X,N=ja(x.top)+ea+12;delete x.left;delete x.top;ga.text(this.html,V,N).attr({zIndex:2}).css(x).add()});if(!l.toolbar)l.toolbar=d();if(q.enabled&&!l.credits){w=q.href;l.credits=ga.text(q.text,0,0).on("click",function(){if(w)location.href=w}).attr({align:q.position.align,zIndex:8}).css(q.style).add().align(q.position)}bd();l.hasRendered=true;if(Zb){qb.appendChild(oa);pc(Zb)}}function H(){var p,q=oa&&oa.parentNode;if(l!==null){Pa(l,"destroy");pb(db,"unload",H);pb(l);for(p=Va.length;p--;)Va[p]= +Va[p].destroy();for(p=Ha.length;p--;)Ha[p]=Ha[p].destroy();u(["title","subtitle","seriesGroup","clipRect","credits","tracker"],function(w){var x=l[w];if(x)l[w]=x.destroy()});u([wc,xc,Kc,Fc,hc,ga,Yb],function(w){w&&w.destroy&&w.destroy()});wc=xc=Kc=Fc=hc=ga=Yb=null;if(oa){oa.innerHTML="";pb(oa);q&&pc(oa);oa=null}clearInterval(Vd);for(p in l)delete l[p];l=null}}function U(){if(!Lc&&db==db.top&&ua.readyState!=="complete")ua.attachEvent("onreadystatechange",function(){ua.detachEvent("onreadystatechange", +U);ua.readyState==="complete"&&U()});else{v();yd();zd();u(a.series||[],function(p){g(p)});l.inverted=va=A(va,a.chart.inverted);j();l.render=Y;l.tracker=Yb=new f(a.tooltip);Y();Pa(l,"load");b&&b.apply(l,[l]);u(l.callbacks,function(p){p.apply(l,[l])})}}Yc=Ca(Yc,Xa.xAxis);od=Ca(od,Xa.yAxis);Xa.xAxis=Xa.yAxis=null;a=Ca(Xa,a);var z=a.chart,M=z.margin;M=Nb(M)?M:[M,M,M,M];var y=A(z.marginTop,M[0]),C=A(z.marginRight,M[1]),Z=A(z.marginBottom,M[2]),Sa=A(z.marginLeft,M[3]),Na=z.spacingTop,Ea=z.spacingRight, +gb=z.spacingBottom,Lb=z.spacingLeft,Rb,uc,Ic,ea,Ib,vb,X,Xb,qb,Zb,oa,Wd,dd,Jc,$a,cb,qd,ad,wc,Kc,Ad,xc,l=this,ke=(M=z.events)&&!!M.click,Bd,gc,hc,ud,jc,Zd,Cd,Aa,Da,Yb,Ec,bd,Fc,$b,ac,Jb,sc=z.showAxes,cd=0,Va=[],Ob,Ha=[],va,ga,rd,Vd,sd,vd,Gc,yd,zd,xd,td,$d,le=function(){function p(G,ha){var na=G.legendItem,Ra=G.legendLine,La=G.legendSymbol,pa=Oa.color,wa=ha?N.itemStyle.color:pa,zb=ha?G.color:pa;pa=ha?G.pointAttr[ib]:{stroke:pa,fill:pa};na&&na.css({fill:wa});Ra&&Ra.attr({stroke:zb});La&&La.attr(pa)}function q(G, +ha,na){var Ra=G.legendItem,La=G.legendLine,pa=G.legendSymbol;G=G.checkbox;Ra&&Ra.attr({x:ha,y:na});La&&La.translate(ha,na-4);pa&&pa.attr({x:ha+pa.xOff,y:na+pa.yOff});if(G){G.x=ha;G.y=na}}function w(){u(za,function(G){var ha=G.checkbox,na=qa.alignAttr;ha&&Ja(ha,{left:na.translateX+G.legendItemWidth+ha.x-40+Ua,top:na.translateY+ha.y-11+Ua})})}function x(G){var ha,na,Ra,La,pa=G.legendItem;La=G.series||G;var wa=La.options,zb=wa&&wa.borderWidth||0;if(!pa){La=/^(bar|pie|area|column)$/.test(La.type);G.legendItem= +pa=ga.text(N.labelFormatter.call(G),0,0).css(G.visible?ab:Oa).on("mouseover",function(){G.setState(Bb);pa.css(xa)}).on("mouseout",function(){pa.css(G.visible?ab:Oa);G.setState()}).on("click",function(){var Gb=function(){G.setVisible()};G.firePointEvent?G.firePointEvent("legendItemClick",null,Gb):Pa(G,"legendItemClick",null,Gb)}).attr({zIndex:2}).add(qa);if(!La&&wa&&wa.lineWidth){var Vb={"stroke-width":wa.lineWidth,zIndex:2};if(wa.dashStyle)Vb.dashstyle=wa.dashStyle;G.legendLine=ga.path([Za,-da-Ma, +0,Ka,-Ma,0]).attr(Vb).add(qa)}if(La)ha=ga.rect(na=-da-Ma,Ra=-11,da,12,2).attr({zIndex:3}).add(qa);else if(wa&&wa.marker&&wa.marker.enabled)ha=ga.symbol(G.symbol,na=-da/2-Ma,Ra=-4,wa.marker.radius).attr({zIndex:3}).add(qa);if(ha){ha.xOff=na+zb%2/2;ha.yOff=Ra+zb%2/2}G.legendSymbol=ha;p(G,G.visible);if(wa&&wa.showCheckbox){G.checkbox=hb("input",{type:"checkbox",checked:G.selected,defaultChecked:G.selected},N.itemCheckboxStyle,oa);Qa(G.checkbox,"click",function(Gb){Pa(G,"checkboxClick",{checked:Gb.target.checked}, +function(){G.select()})})}}ha=pa.getBBox();na=G.legendItemWidth=N.itemWidth||da+Ma+ha.width+R;B=ha.height;if(ta&&o-s+na>(fb||$a-2*R-s)){o=s;I+=B}ba=I;q(G,o,I);if(ta)o+=na;else I+=B;ya=fb||Ia(ta?o-s:na,ya)}function V(){o=s;I=L;ba=ya=0;qa||(qa=ga.g("legend").attr({zIndex:7}).add());za=[];u(Hb,function(Ra){var La=Ra.options;if(La.showInLegend)za=za.concat(La.legendType==="point"?Ra.data:Ra)});Hd(za,function(Ra,La){return(Ra.options.legendIndex||0)-(La.options.legendIndex||0)});cc&&za.reverse();u(za, +x);$b=fb||ya;ac=ba-L+B;if(ca||la){$b+=2*R;ac+=2*R;if(D){if($b>0&&ac>0){D[D.isNew?"attr":"animate"](D.crisp(null,null,null,$b,ac));D.isNew=false}}else{D=ga.rect(0,0,$b,ac,N.borderRadius,ca||0).attr({stroke:N.borderColor,"stroke-width":ca||0,fill:la||jb}).add(qa).shadow(N.shadow);D.isNew=true}D[za.length?"show":"hide"]()}for(var G=["left","right","top","bottom"],ha,na=4;na--;){ha=G[na];if(eb[ha]&&eb[ha]!=="auto"){N[na<2?"align":"verticalAlign"]=ha;N[na<2?"x":"y"]=ja(eb[ha])*(na%2?-1:1)}}za.length&& +qa.align(sa(N,{width:$b,height:ac}),true,Rb);cd||w()}var N=l.options.legend;if(N.enabled){var ta=N.layout==="horizontal",da=N.symbolWidth,Ma=N.symbolPadding,za,eb=N.style,ab=N.itemStyle,xa=N.itemHoverStyle,Oa=N.itemHiddenStyle,R=ja(eb.padding),L=18,s=4+R+da+Ma,o,I,ba,B=0,D,ca=N.borderWidth,la=N.backgroundColor,qa,ya,fb=N.width,Hb=l.series,cc=N.reversed;V();Qa(l,"endResize",w);return{colorizeItem:p,destroyItem:function(G){var ha=G.checkbox;u(["legendItem","legendLine","legendSymbol"],function(na){G[na]&& +G[na].destroy()});ha&&pc(G.checkbox)},renderLegend:V,destroy:function(){if(D)D=D.destroy();if(qa)qa=qa.destroy()}}}};gc=function(p,q){return p>=0&&p<=Da&&q>=0&&q<=Aa};$d=function(){Pa(l,"selection",{resetSelection:true},td);l.toolbar.remove("zoom")};td=function(p){var q=Xa.lang,w=l.pointCount<100;l.toolbar.add("zoom",q.resetZoom,q.resetZoomTitle,$d);!p||p.resetSelection?u(Va,function(x){x.setExtremes(null,null,false,w)}):u(p.xAxis.concat(p.yAxis),function(x){var V=x.axis;if(l.tracker[V.isXAxis?"zoomX": +"zoomY"])V.setExtremes(x.min,x.max,false,w)});i()};Gc=function(){var p=a.legend,q=A(p.margin,10),w=p.x,x=p.y,V=p.align,N=p.verticalAlign,ta;yd();if((l.title||l.subtitle)&&!K(y))if(ta=Ia(l.title&&!uc.floating&&!uc.verticalAlign&&uc.y||0,l.subtitle&&!Ic.floating&&!Ic.verticalAlign&&Ic.y||0))ea=Ia(ea,ta+A(uc.margin,15)+Na);if(p.enabled&&!p.floating)if(V==="right")K(C)||(Ib=Ia(Ib,$b-w+q+Ea));else if(V==="left")K(Sa)||(X=Ia(X,$b+w+q+Lb));else if(N==="top")K(y)||(ea=Ia(ea,ac+x+q+Na));else if(N==="bottom")K(Z)|| +(vb=Ia(vb,ac-x+q+gb));sc&&u(Va,function(da){da.getOffset()});K(Sa)||(X+=Xb[3]);K(y)||(ea+=Xb[0]);K(Z)||(vb+=Xb[2]);K(C)||(Ib+=Xb[1]);zd()};xd=function(p,q,w){var x=l.title,V=l.subtitle;cd+=1;oc(w,l);ad=cb;qd=$a;l.chartWidth=$a=W(p);l.chartHeight=cb=W(q);Ja(oa,{width:$a+Ua,height:cb+Ua});ga.setSize($a,cb,w);Da=$a-X-Ib;Aa=cb-ea-vb;Ob=null;u(Va,function(N){N.isDirty=true;N.setScale()});u(Ha,function(N){N.isDirty=true});l.isDirtyLegend=true;l.isDirtyBox=true;Gc();x&&x.align(null,null,Rb);V&&V.align(null, +null,Rb);i(w);ad=null;Pa(l,"resize");Bc===false?T():setTimeout(T,Bc&&Bc.duration||500)};zd=function(){l.plotLeft=X=W(X);l.plotTop=ea=W(ea);l.plotWidth=Da=W($a-X-Ib);l.plotHeight=Aa=W(cb-ea-vb);l.plotSizeX=va?Aa:Da;l.plotSizeY=va?Da:Aa;Rb={x:Lb,y:Na,width:$a-Lb-Ea,height:cb-Na-gb}};yd=function(){ea=A(y,Na);Ib=A(C,Ea);vb=A(Z,gb);X=A(Sa,Lb);Xb=[0,0,0,0]};vd=function(){var p=z.borderWidth||0,q=z.backgroundColor,w=z.plotBackgroundColor,x=z.plotBackgroundImage,V,N={x:X,y:ea,width:Da,height:Aa};V=p+(z.shadow? +8:0);if(p||q)if(wc)wc.animate(wc.crisp(null,null,null,$a-V,cb-V));else wc=ga.rect(V/2,V/2,$a-V,cb-V,z.borderRadius,p).attr({stroke:z.borderColor,"stroke-width":p,fill:q||jb}).add().shadow(z.shadow);if(w)if(Kc)Kc.animate(N);else Kc=ga.rect(X,ea,Da,Aa,0).attr({fill:w}).add().shadow(z.plotShadow);if(x)if(Ad)Ad.animate(N);else Ad=ga.image(x,X,ea,Da,Aa).add();if(z.plotBorderWidth)if(xc)xc.animate(xc.crisp(null,X,ea,Da,Aa));else xc=ga.rect(X,ea,Da,Aa,0,z.plotBorderWidth).attr({stroke:z.plotBorderColor, +"stroke-width":z.plotBorderWidth,zIndex:4}).add();l.isDirtyBox=false};Qa(db,"unload",H);z.reflow!==false&&Qa(l,"load",P);if(M)for(Bd in M)Qa(l,Bd,M[Bd]);l.options=a;l.series=Ha;l.addSeries=function(p,q,w){var x;if(p){oc(w,l);q=A(q,true);Pa(l,"addSeries",{options:p},function(){x=g(p);x.isDirty=true;l.isDirtyLegend=true;q&&l.redraw()})}return x};l.animation=A(z.animation,true);l.destroy=H;l.get=function(p){var q,w,x;for(q=0;q=a)this.color=0},wrapSymbol:function(a){if(this.symbol>=a)this.symbol=0}};Nc={init:function(a,b,c){b=b||"";var d=a.shift,e=b.indexOf("C")>-1,f=e?7:3,g;b=b.split(" ");c=[].concat(c);var h, +i,j=function(m){for(g=m.length;g--;)m[g]===Za&&m.splice(g+1,0,m[g+1],m[g+2],m[g+1],m[g+2])};if(e){j(b);j(c)}if(a.isArea){h=b.splice(b.length-6,6);i=c.splice(c.length-6,6)}if(d){c=[].concat(c).splice(0,f).concat(c);a.shift=false}if(b.length)for(a=c.length;b.length255)b[e]=255}}return this},setOpacity:function(d){b[3]=d;return this}}};Uc.prototype={init:function(a,b){this.element=ua.createElementNS("http://www.w3.org/2000/svg", +b);this.renderer=a},animate:function(a,b,c){if(b=A(b,Bc,true)){b=Ca(b);if(c)b.complete=c;gd(this,a,b)}else{this.attr(a);c&&c()}},attr:function(a,b){var c,d,e,f,g=this.element,h=g.nodeName,i=this.renderer,j,m=this.shadows,v=this.htmlNode,P,T=this;if(Sb(a)&&K(b)){c=a;a={};a[c]=b}if(Sb(a)){c=a;if(h==="circle")c={x:"cx",y:"cy"}[c]||c;else if(c==="strokeWidth")c="stroke-width";T=Ga(g,c)||this[c]||0;if(c!=="d"&&c!=="visibility")T=parseFloat(T)}else for(c in a){j=false;d=a[c];if(c==="d"){if(d&&d.join)d= +d.join(" ");if(/(NaN| {2}|^$)/.test(d))d="M 0 0";this.d=d}else if(c==="x"&&h==="text"){for(e=0;eg||!K(g)&&K(b))){d.insertBefore(f,a);return this}}d.appendChild(f);this.added=true;return this},safeRemoveChild:function(a){var b=a.parentNode;b&&b.removeChild(a)},destroy:function(){var a=this,b=a.element|| +{},c=a.shadows,d,e;b.onclick=b.onmouseout=b.onmouseover=b.onmousemove=null;Hc(a);if(a.clipPath)a.clipPath=a.clipPath.destroy();if(a.stops){for(e=0;e/g,'').replace(/<(i|em)>/g,'').replace(//g,"").split(//g),d=b.childNodes,e=/style="([^"]+)"/,f=/href="([^"]+)"/,g=Ga(b,"x"),h=a.styles,i=h&&a.useHTML&&!this.forExport,j=a.htmlNode,m=h&&ja(h.width),v=h&&h.lineHeight,P,T=d.length;T--;)b.removeChild(d[T]);m&&!a.added&& +this.box.appendChild(b);u(c,function(Y,H){var U,z=0,M;Y=Y.replace(//g,"|||");U=Y.split("|||");u(U,function(y){if(y!==""||U.length===1){var C={},Z=ua.createElementNS("http://www.w3.org/2000/svg","tspan");e.test(y)&&Ga(Z,"style",y.match(e)[1].replace(/(;| |^)color([ :])/,"$1fill$2"));if(f.test(y)){Ga(Z,"onclick",'location.href="'+y.match(f)[1]+'"');Ja(Z,{cursor:"pointer"})}y=(y.replace(/<(.|\n)*?>/g,"")||" ").replace(/</g,"<").replace(/>/g,">");Z.appendChild(ua.createTextNode(y)); +if(z)C.dx=3;else C.x=g;if(!z){if(H){!Lc&&a.renderer.forExport&&Ja(Z,{display:"block"});M=db.getComputedStyle&&ja(db.getComputedStyle(P,null).getPropertyValue("line-height"));if(!M||isNaN(M))M=v||P.offsetHeight||18;Ga(Z,"dy",M)}P=Z}Ga(Z,C);b.appendChild(Z);z++;if(m){y=y.replace(/-/g,"- ").split(" ");for(var Sa,Na=[];y.length||Na.length;){Sa=b.getBBox().width;C=Sa>m;if(!C||y.length===1){y=Na;Na=[];if(y.length){Z=ua.createElementNS("http://www.w3.org/2000/svg","tspan");Ga(Z,{dy:v||16,x:g});b.appendChild(Z); +if(Sa>m)m=Sa}}else{Z.removeChild(Z.firstChild);Na.unshift(y.pop())}y.length&&Z.appendChild(ua.createTextNode(y.join(" ").replace(/- /g,"-")))}}}})});if(i){if(!j)j=a.htmlNode=hb("span",null,sa(h,{position:ic,top:0,left:0}),this.box.parentNode);j.innerHTML=a.textStr;for(T=d.length;T--;)d[T].style.visibility=ob}},crispLine:function(a,b){if(a[1]===a[4])a[1]=a[4]=W(a[1])+b%2/2;if(a[2]===a[5])a[2]=a[5]=W(a[2])+b%2/2;return a},path:function(a){return this.createElement("path").attr({d:a,fill:jb})},circle:function(a, +b,c){a=Nb(a)?a:{x:a,y:b,r:c};return this.createElement("circle").attr(a)},arc:function(a,b,c,d,e,f){if(Nb(a)){b=a.y;c=a.r;d=a.innerR;e=a.start;f=a.end;a=a.x}return this.symbol("arc",a||0,b||0,c||0,{innerR:d||0,start:e||0,end:f||0})},rect:function(a,b,c,d,e,f){if(Nb(a)){b=a.y;c=a.width;d=a.height;e=a.r;f=a.strokeWidth;a=a.x}e=this.createElement("rect").attr({rx:e,ry:e,fill:jb});return e.attr(e.crisp(f,a,b,Ia(c,0),Ia(d,0)))},setSize:function(a,b,c){var d=this.alignedObjects,e=d.length;this.width=a; +this.height=b;for(this.boxWrapper[A(c,true)?"animate":"attr"]({width:a,height:b});e--;)d[e].align()},g:function(a){var b=this.createElement("g");return K(a)?b.attr({"class":vc+a}):b},image:function(a,b,c,d,e){var f={preserveAspectRatio:jb};arguments.length>1&&sa(f,{x:b,y:c,width:d,height:e});f=this.createElement("image").attr(f);f.element.setAttributeNS?f.element.setAttributeNS("http://www.w3.org/1999/xlink","href",a):f.element.setAttribute("hc-svg-href",a);return f},symbol:function(a,b,c,d,e){var f, +g=this.symbols[a];g=g&&g(W(b),W(c),d,e);var h=/^url\((.*?)\)$/,i;if(g){f=this.path(g);sa(f,{symbolName:a,x:b,y:c,r:d});e&&sa(f,e)}else if(h.test(a)){var j=function(m,v){m.attr({width:v[0],height:v[1]}).translate(-W(v[0]/2),-W(v[1]/2))};i=a.match(h)[1];a=be[i];f=this.image(i).attr({x:b,y:c});if(a)j(f,a);else{f.attr({width:0,height:0});hb("img",{onload:function(){j(f,be[i]=[this.width,this.height])},src:i})}}else f=this.circle(b,c,d);return f},symbols:{square:function(a,b,c){c=0.707*c;return[Za,a-c, +b-c,Ka,a+c,b-c,a+c,b+c,a-c,b+c,"Z"]},triangle:function(a,b,c){return[Za,a,b-1.33*c,Ka,a+c,b+0.67*c,a-c,b+0.67*c,"Z"]},"triangle-down":function(a,b,c){return[Za,a,b+1.33*c,Ka,a-c,b-0.67*c,a+c,b-0.67*c,"Z"]},diamond:function(a,b,c){return[Za,a,b-c,Ka,a+c,b,a,b+c,a-c,b,"Z"]},arc:function(a,b,c,d){var e=d.start,f=d.end-1.0E-6,g=d.innerR,h=rb(e),i=Cb(e),j=rb(f);f=Cb(f);d=d.end-e');if(b){c=b===Tb||b==="span"||b==="img"?c.join(""):a.prepVML(c);this.element=hb(c)}this.renderer=a},add:function(a){var b=this.renderer,c=this.element,d=b.box;d=a?a.element||a:d;a&&a.inverted&&b.invertChild(c,d);Mc&&d.gVis===ob&&Ja(c,{visibility:ob});d.appendChild(c);this.added=true;this.alignOnAdd&& +this.updateTransform();return this},attr:function(a,b){var c,d,e,f=this.element||{},g=f.style,h=f.nodeName,i=this.renderer,j=this.symbolName,m,v,P=this.shadows,T=this;if(Sb(a)&&K(b)){c=a;a={};a[c]=b}if(Sb(a)){c=a;T=c==="strokeWidth"||c==="stroke-width"?this.strokeweight:this[c]}else for(c in a){d=a[c];m=false;if(j&&/^(x|y|r|start|end|width|height|innerR)/.test(c)){if(!v){this.symbolAttr(a);v=true}m=true}else if(c==="d"){d=d||[];this.d=d.join(" ");e=d.length;for(m=[];e--;)m[e]=lc(d[e])?W(d[e]*10)- +5:d[e]==="Z"?"x":d[e];d=m.join(" ")||"x";f.path=d;if(P)for(e=P.length;e--;)P[e].path=d;m=true}else if(c==="zIndex"||c==="visibility"){if(Mc&&c==="visibility"&&h==="DIV"){f.gVis=d;m=f.childNodes;for(e=m.length;e--;)Ja(m[e],{visibility:d});if(d===Ab)d=null}if(d)g[c]=d;m=true}else if(/^(width|height)$/.test(c)){this[c]=d;if(this.updateClipping){this[c]=d;this.updateClipping()}else g[c]=d;m=true}else if(/^(x|y)$/.test(c)){this[c]=d;if(f.tagName==="SPAN")this.updateTransform();else g[{x:"left",y:"top"}[c]]= +d}else if(c==="class")f.className=d;else if(c==="stroke"){d=i.color(d,f,c);c="strokecolor"}else if(c==="stroke-width"||c==="strokeWidth"){f.stroked=d?true:false;c="strokeweight";this[c]=d;if(lc(d))d+=Ua}else if(c==="dashstyle"){(f.getElementsByTagName("stroke")[0]||hb(i.prepVML([""]),null,null,f))[c]=d||"solid";this.dashstyle=d;m=true}else if(c==="fill")if(h==="SPAN")g.color=d;else{f.filled=d!==jb?true:false;d=i.color(d,f,c);c="fillcolor"}else if(c==="translateX"||c==="translateY"||c==="rotation"|| +c==="align"){if(c==="align")c="textAlign";this[c]=d;this.updateTransform();m=true}else if(c==="text"){this.bBox=null;f.innerHTML=d;m=true}if(P&&c==="visibility")for(e=P.length;e--;)P[e].style[c]=d;if(!m)if(Mc)f[c]=d;else Ga(f,c,d)}return T},clip:function(a){var b=this,c=a.members;c.push(b);b.destroyClip=function(){nc(c,b)};return b.css(a.getCSS(b.inverted))},css:function(a){var b=this.element;if(b=a&&b.tagName==="SPAN"&&a.width){delete a.width;this.textWidth=b;this.updateTransform()}this.styles=sa(this.styles, +a);Ja(this.element,a);return this},safeRemoveChild:function(a){a.parentNode&&pc(a)},destroy:function(){this.destroyClip&&this.destroyClip();return Uc.prototype.destroy.apply(this)},empty:function(){for(var a=this.element.childNodes,b=a.length,c;b--;){c=a[b];c.parentNode.removeChild(c)}},getBBox:function(){var a=this.element,b=this.bBox;if(!b){if(a.nodeName==="text")a.style.position=ic;b=this.bBox={x:a.offsetLeft,y:a.offsetTop,width:a.offsetWidth,height:a.offsetHeight}}return b},on:function(a,b){this.element["on"+ +a]=function(){var c=db.event;c.target=c.srcElement;b(c)};return this},updateTransform:function(){if(this.added){var a=this,b=a.element,c=a.translateX||0,d=a.translateY||0,e=a.x||0,f=a.y||0,g=a.textAlign||"left",h={left:0,center:0.5,right:1}[g],i=g&&g!=="left";if(c||d)a.css({marginLeft:c,marginTop:d});a.inverted&&u(b.childNodes,function(z){a.renderer.invertChild(z,b)});if(b.tagName==="SPAN"){var j,m;c=a.rotation;var v;j=0;d=1;var P=0,T;v=ja(a.textWidth);var Y=a.xCorr||0,H=a.yCorr||0,U=[c,g,b.innerHTML, +a.textWidth].join(",");if(U!==a.cTT){if(K(c)){j=c*ae;d=rb(j);P=Cb(j);Ja(b,{filter:c?["progid:DXImageTransform.Microsoft.Matrix(M11=",d,", M12=",-P,", M21=",P,", M22=",d,", sizingMethod='auto expand')"].join(""):jb})}j=b.offsetWidth;m=b.offsetHeight;if(j>v){Ja(b,{width:v+Ua,display:"block",whiteSpace:"normal"});j=v}v=W((ja(b.style.fontSize)||12)*1.2);Y=d<0&&-j;H=P<0&&-m;T=d*P<0;Y+=P*v*(T?1-h:h);H-=d*v*(c?T?h:1-h:1);if(i){Y-=j*h*(d<0?-1:1);if(c)H-=m*h*(P<0?-1:1);Ja(b,{textAlign:g})}a.xCorr=Y;a.yCorr= +H}Ja(b,{left:e+Y,top:f+H});a.cTT=U}}else this.alignOnAdd=true},shadow:function(a,b){var c=[],d,e=this.element,f=this.renderer,g,h=e.style,i,j=e.path;if(j&&typeof j.value!=="string")j="x";if(a){for(d=1;d<=3;d++){i=[''];g=hb(f.prepVML(i),null,{left:ja(h.left)+1,top:ja(h.top)+1});i=[''];hb(f.prepVML(i),null,null,g);b?b.element.appendChild(g): +e.parentNode.insertBefore(g,e);c.push(g)}this.shadows=c}return this}});ma=function(){this.init.apply(this,arguments)};ma.prototype=Ca(ed.prototype,{Element:Db,isIE8:yc.indexOf("MSIE 8.0")>-1,init:function(a,b,c){var d;this.alignedObjects=[];d=this.createElement(Tb);a.appendChild(d.element);this.box=d.element;this.boxWrapper=d;this.setSize(b,c,false);if(!ua.namespaces.hcv){ua.namespaces.add("hcv","urn:schemas-microsoft-com:vml");ua.createStyleSheet().cssText="hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke{ behavior:url(#default#VML); display: inline-block; } "}}, +clipRect:function(a,b,c,d){var e=this.createElement();return sa(e,{members:[],left:a,top:b,width:c,height:d,getCSS:function(f){var g=this.top,h=this.left,i=h+this.width,j=g+this.height;g={clip:"rect("+W(f?h:g)+"px,"+W(f?j:i)+"px,"+W(f?i:j)+"px,"+W(f?g:h)+"px)"};!f&&Mc&&sa(g,{width:i+Ua,height:j+Ua});return g},updateClipping:function(){u(e.members,function(f){f.css(e.getCSS(f.inverted))})}})},color:function(a,b,c){var d,e=/^rgba/;if(a&&a.linearGradient){var f,g,h=a.linearGradient,i,j,m,v;u(a.stops, +function(P,T){if(e.test(P[1])){d=bc(P[1]);f=d.get("rgb");g=d.get("a")}else{f=P[1];g=1}if(T){m=f;v=g}else{i=f;j=g}});a=90-Fa.atan((h[3]-h[1])/(h[2]-h[0]))*180/kc;a=["<",c,' colors="0% ',i,",100% ",m,'" angle="',a,'" opacity="',v,'" o:opacity2="',j,'" type="gradient" focus="100%" />'];hb(this.prepVML(a),null,null,b)}else if(e.test(a)&&b.tagName!=="IMG"){d=bc(a);a=["<",c,' opacity="',d.get("a"),'"/>'];hb(this.prepVML(a),null,null,b);return d.get("rgb")}else{b=b.getElementsByTagName(c);if(b.length)b[0].opacity= +1;return a}},prepVML:function(a){var b=this.isIE8;a=a.join("");if(b){a=a.replace("/>",' xmlns="urn:schemas-microsoft-com:vml" />');a=a.indexOf('style="')===-1?a.replace("/>",' style="display:inline-block;behavior:url(#default#VML);" />'):a.replace('style="','style="display:inline-block;behavior:url(#default#VML);')}else a=a.replace("<","1&&f.css({left:b,top:c,width:d,height:e});return f},rect:function(a,b,c,d,e,f){if(Nb(a)){b=a.y;c=a.width;d=a.height;e=a.r;f=a.strokeWidth; +a=a.x}var g=this.symbol("rect");g.r=e;return g.attr(g.crisp(f,a,b,Ia(c,0),Ia(d,0)))},invertChild:function(a,b){var c=b.style;Ja(a,{flip:"x",left:ja(c.width)-10,top:ja(c.height)-10,rotation:-90})},symbols:{arc:function(a,b,c,d){var e=d.start,f=d.end,g=rb(e),h=Cb(e),i=rb(f),j=Cb(f);d=d.innerR;var m=0.07/c,v=d&&0.1/d||0;if(f-e===0)return["x"];else if(2*kc-f+e',this.name||b.name,": ",!a?"x = "+(this.name||this.x)+", ":"","",!a?"y = ":"",this.y,""].join("")},update:function(a,b,c){var d=this,e=d.series,f=d.graphic,g=e.chart;b=A(b,true);d.firePointEvent("update",{options:a},function(){d.applyOptions(a);if(Nb(a)){e.getAttribs();f&&f.attr(d.pointAttr[e.state])}e.isDirty=true;b&&g.redraw(c)})},remove:function(a,b){var c= +this,d=c.series,e=d.chart,f=d.data;oc(b,e);a=A(a,true);c.firePointEvent("remove",null,function(){nc(f,c);c.destroy();d.isDirty=true;a&&e.redraw()})},firePointEvent:function(a,b,c){var d=this,e=this.series.options;if(e.point.events[a]||d.options&&d.options.events&&d.options.events[a])this.importEvents();if(a==="click"&&e.allowPointSelect)c=function(f){d.select(null,f.ctrlKey||f.metaKey||f.shiftKey)};Pa(this,a,b,c)},importEvents:function(){if(!this.hasImportedEvents){var a=Ca(this.series.options.point, +this.options).events,b;this.events=a;for(b in a)Qa(this,b,a[b]);this.hasImportedEvents=true}},setState:function(a){var b=this.series,c=b.options.states,d=xb[b.type].marker&&b.options.marker,e=d&&!d.enabled,f=(d=d&&d.states[a])&&d.enabled===false,g=b.stateMarkerGraphic,h=b.chart,i=this.pointAttr;a=a||ib;if(!(a===this.state||this.selected&&a!=="select"||c[a]&&c[a].enabled===false||a&&(f||e&&!d.enabled))){if(this.graphic)this.graphic.attr(i[a]);else{if(a){if(!g)b.stateMarkerGraphic=g=h.renderer.circle(0, +0,i[a].r).attr(i[a]).add(b.group);g.translate(this.plotX,this.plotY)}if(g)g[a?"show":"hide"]()}this.state=a}}};var sb=function(){};sb.prototype={isCartesian:true,type:"line",pointClass:Oc,pointAttrToOptions:{stroke:"lineColor","stroke-width":"lineWidth",fill:"fillColor",r:"radius"},init:function(a,b){var c,d;d=a.series.length;this.chart=a;b=this.setOptions(b);sa(this,{index:d,options:b,name:b.name||"Series "+(d+1),state:ib,pointAttr:{},visible:b.visible!==false,selected:b.selected===true});d=b.events; +for(c in d)Qa(this,c,d[c]);if(d&&d.click||b.point&&b.point.events&&b.point.events.click||b.allowPointSelect)a.runTrackerClick=true;this.getColor();this.getSymbol();this.setData(b.data,false)},autoIncrement:function(){var a=this.options,b=this.xIncrement;b=A(b,a.pointStart,0);this.pointInterval=A(this.pointInterval,a.pointInterval,1);this.xIncrement=b+this.pointInterval;return b},cleanData:function(){var a=this.chart,b=this.data,c,d,e=a.smallestInterval,f,g;Hd(b,function(h,i){return h.x-i.x});if(this.options.connectNulls)for(g= +b.length-1;g>=0;g--)b[g].y===null&&b[g-1]&&b[g+1]&&b.splice(g,1);for(g=b.length-1;g>=0;g--)if(b[g-1]){f=b[g].x-b[g-1].x;if(f>0&&(d===Wa||fa+1&&b.push(c.slice(a+1,e));a=e}else e===c.length-1&&b.push(c.slice(a+1,e+1))});this.segments=b},setOptions:function(a){var b=this.chart.options.plotOptions;return Ca(b[this.type],b.series,a)},getColor:function(){var a= +this.chart.options.colors,b=this.chart.counters;this.color=this.options.color||a[b.color++]||"#0000ff";b.wrapColor(a.length)},getSymbol:function(){var a=this.chart.options.symbols,b=this.chart.counters;this.symbol=this.options.marker.symbol||a[b.symbol++];b.wrapSymbol(a.length)},addPoint:function(a,b,c,d){var e=this.data,f=this.graph,g=this.area,h=this.chart;a=(new this.pointClass).init(this,a);oc(d,h);if(f&&c)f.shift=c;if(g){g.shift=c;g.isArea=true}b=A(b,true);e.push(a);c&&e[0].remove(false);this.getAttribs(); +this.isDirty=true;b&&h.redraw()},setData:function(a,b){var c=this,d=c.data,e=c.initialColor,f=c.chart,g=d&&d.length||0;c.xIncrement=null;if(K(e))f.counters.color=e;for(a=tc(zc(a||[]),function(h){return(new c.pointClass).init(c,h)});g--;)d[g].destroy();c.data=a;c.cleanData();c.getSegments();c.getAttribs();c.isDirty=true;f.isDirtyBox=true;A(b,true)&&f.redraw(false)},remove:function(a,b){var c=this,d=c.chart;a=A(a,true);if(!c.isRemoving){c.isRemoving=true;Pa(c,"remove",null,function(){c.destroy();d.isDirtyLegend= +d.isDirtyBox=true;a&&d.redraw(b)})}c.isRemoving=false},translate:function(){for(var a=this.chart,b=this.options.stacking,c=this.xAxis.categories,d=this.yAxis,e=this.data,f=e.length;f--;){var g=e[f],h=g.x,i=g.y,j=g.low,m=d.stacks[(i<0?"-":"")+this.stackKey];g.plotX=this.xAxis.translate(h);if(b&&this.visible&&m&&m[h]){j=m[h];h=j.total;j.cum=j=j.cum-i;i=j+i;if(b==="percent"){j=h?j*100/h:0;i=h?i*100/h:0}g.percentage=h?g.y*100/h:0;g.stackTotal=h}if(K(j))g.yBottom=d.translate(j,0,1,0,1);if(i!==null)g.plotY= +d.translate(i,0,1,0,1);g.clientX=a.inverted?a.plotHeight-g.plotX:g.plotX;g.category=c&&c[g.x]!==Wa?c[g.x]:g.x}},setTooltipPoints:function(a){var b=this.chart,c=b.inverted,d=[],e=W((c?b.plotTop:b.plotLeft)+b.plotSizeX),f,g,h=[];if(a)this.tooltipPoints=null;u(this.segments,function(i){d=d.concat(i)});if(this.xAxis&&this.xAxis.reversed)d=d.reverse();u(d,function(i,j){f=d[j-1]?d[j-1]._high+1:0;for(g=i._high=d[j+1]?lb((i.plotX+(d[j+1]?d[j+1].plotX:e))/2):e;f<=g;)h[c?e-f++:f++]=i});this.tooltipPoints=h}, +onMouseOver:function(){var a=this.chart,b=a.hoverSeries;if(!(!Kb&&a.mouseIsDown)){b&&b!==this&&b.onMouseOut();this.options.events.mouseOver&&Pa(this,"mouseOver");this.tracker&&this.tracker.toFront();this.setState(Bb);a.hoverSeries=this}},onMouseOut:function(){var a=this.options,b=this.chart,c=b.tooltip,d=b.hoverPoint;d&&d.onMouseOut();this&&a.events.mouseOut&&Pa(this,"mouseOut");c&&!a.stickyTracking&&c.hide();this.setState();b.hoverSeries=null},animate:function(a){var b=this.chart,c=this.clipRect, +d=this.options.animation;if(d&&!Nb(d))d={};if(a){if(!c.isAnimating){c.attr("width",0);c.isAnimating=true}}else{c.animate({width:b.plotSizeX},d);this.animate=null}},drawPoints:function(){var a,b=this.data,c=this.chart,d,e,f,g,h,i;if(this.options.marker.enabled)for(f=b.length;f--;){g=b[f];d=g.plotX;e=g.plotY;i=g.graphic;if(e!==Wa&&!isNaN(e)){a=g.pointAttr[g.selected?"select":ib];h=a.r;if(i)i.animate({x:d,y:e,r:h});else g.graphic=c.renderer.symbol(A(g.marker&&g.marker.symbol,this.symbol),d,e,h).attr(a).add(this.group)}}}, +convertAttribs:function(a,b,c,d){var e=this.pointAttrToOptions,f,g,h={};a=a||{};b=b||{};c=c||{};d=d||{};for(f in e){g=e[f];h[f]=A(a[g],b[f],c[f],d[f])}return h},getAttribs:function(){var a=this,b=xb[a.type].marker?a.options.marker:a.options,c=b.states,d=c[Bb],e,f=a.color,g={stroke:f,fill:f},h=a.data,i=[],j,m=a.pointAttrToOptions,v;if(a.options.marker){d.radius=d.radius||b.radius+2;d.lineWidth=d.lineWidth||b.lineWidth+1}else d.color=d.color||bc(d.color||f).brighten(d.brightness).get();i[ib]=a.convertAttribs(b, +g);u([Bb,"select"],function(P){i[P]=a.convertAttribs(c[P],i[ib])});a.pointAttr=i;for(f=h.length;f--;){g=h[f];if((b=g.options&&g.options.marker||g.options)&&b.enabled===false)b.radius=0;e=false;if(g.options)for(v in m)if(K(b[m[v]]))e=true;if(e){j=[];c=b.states||{};e=c[Bb]=c[Bb]||{};if(!a.options.marker)e.color=bc(e.color||g.options.color).brighten(e.brightness||d.brightness).get();j[ib]=a.convertAttribs(b,i[ib]);j[Bb]=a.convertAttribs(c[Bb],i[Bb],j[ib]);j.select=a.convertAttribs(c.select,i.select, +j[ib])}else j=i;g.pointAttr=j}},destroy:function(){var a=this,b=a.chart,c=a.clipRect,d=/\/5[0-9\.]+ (Safari|Mobile)\//.test(yc),e,f;Pa(a,"destroy");pb(a);a.legendItem&&a.chart.legend.destroyItem(a);u(a.data,function(g){g.destroy()});if(c&&c!==b.clipRect)a.clipRect=c.destroy();u(["area","graph","dataLabelsGroup","group","tracker"],function(g){if(a[g]){e=d&&g==="group"?"hide":"destroy";a[g][e]()}});if(b.hoverSeries===a)b.hoverSeries=null;nc(b.series,a);for(f in a)delete a[f]},drawDataLabels:function(){if(this.options.dataLabels.enabled){var a, +b,c=this.data,d=this.options,e=d.dataLabels,f,g=this.dataLabelsGroup,h=this.chart,i=h.renderer,j=h.inverted,m=this.type,v;v=d.stacking;var P=m==="column"||m==="bar",T=e.verticalAlign===null,Y=e.y===null;if(P)if(v){if(T)e=Ca(e,{verticalAlign:"middle"});if(Y)e=Ca(e,{y:{top:14,middle:4,bottom:-6}[e.verticalAlign]})}else if(T)e=Ca(e,{verticalAlign:"top"});if(g)g.translate(h.plotLeft,h.plotTop);else g=this.dataLabelsGroup=i.g("data-labels").attr({visibility:this.visible?Ab:ob,zIndex:6}).translate(h.plotLeft, +h.plotTop).add();v=e.color;if(v==="auto")v=null;e.style.color=A(v,this.color,"black");u(c,function(H){var U=H.barX,z=U&&U+H.barW/2||H.plotX||-999,M=A(H.plotY,-999),y=H.dataLabel,C=e.align,Z=Y?H.y>=0?-6:12:e.y;f=e.formatter.call(H.getLabelConfig());a=(j?h.plotWidth-M:z)+e.x;b=(j?h.plotHeight-z:M)+Z;if(m==="column")a+={left:-1,right:1}[C]*H.barW/2||0;if(j&&H.y<0){C="right";a-=10}if(y){if(j&&!e.y)b=b+ja(y.styles.lineHeight)*0.9-y.getBBox().height/2;y.attr({text:f}).animate({x:a,y:b})}else if(K(f)){y= +H.dataLabel=i.text(f,a,b).attr({align:C,rotation:e.rotation,zIndex:1}).css(e.style).add(g);j&&!e.y&&y.attr({y:b+ja(y.styles.lineHeight)*0.9-y.getBBox().height/2})}if(P&&d.stacking&&y){z=H.barY;M=H.barW;H=H.barH;y.align(e,null,{x:j?h.plotWidth-z-H:U,y:j?h.plotHeight-U-M:z,width:j?H:M,height:j?M:H})}})}},drawGraph:function(){var a=this,b=a.options,c=a.graph,d=[],e,f=a.area,g=a.group,h=b.lineColor||a.color,i=b.lineWidth,j=b.dashStyle,m,v=a.chart.renderer,P=a.yAxis.getThreshold(b.threshold||0),T=/^area/.test(a.type), +Y=[],H=[];u(a.segments,function(U){m=[];u(U,function(C,Z){if(a.getPointSpline)m.push.apply(m,a.getPointSpline(U,C,Z));else{m.push(Z?Ka:Za);Z&&b.step&&m.push(C.plotX,U[Z-1].plotY);m.push(C.plotX,C.plotY)}});if(U.length>1)d=d.concat(m);else Y.push(U[0]);if(T){var z=[],M,y=m.length;for(M=0;M=0;M--)z.push(U[M].plotX,U[M].yBottom);else z.push(Ka,U[U.length-1].plotX,P,Ka,U[0].plotX,P);H=H.concat(z)}}); +a.graphPath=d;a.singlePoints=Y;if(T){e=A(b.fillColor,bc(a.color).setOpacity(b.fillOpacity||0.75).get());if(f)f.animate({d:H});else a.area=a.chart.renderer.path(H).attr({fill:e}).add(g)}if(c){Hc(c);c.animate({d:d})}else if(i){c={stroke:h,"stroke-width":i};if(j)c.dashstyle=j;a.graph=v.path(d).attr(c).add(g).shadow(b.shadow)}},render:function(){var a=this,b=a.chart,c,d,e=a.options,f=e.animation,g=f&&a.animate;f=g?f&&f.duration||500:0;var h=a.clipRect,i=b.renderer;if(!h){h=a.clipRect=!b.hasRendered&& +b.clipRect?b.clipRect:i.clipRect(0,0,b.plotSizeX,b.plotSizeY);if(!b.clipRect)b.clipRect=h}if(!a.group){c=a.group=i.g("series");if(b.inverted){d=function(){c.attr({width:b.plotWidth,height:b.plotHeight}).invert()};d();Qa(b,"resize",d);Qa(a,"destroy",function(){pb(b,"resize",d)})}c.clip(a.clipRect).attr({visibility:a.visible?Ab:ob,zIndex:e.zIndex}).translate(b.plotLeft,b.plotTop).add(b.seriesGroup)}a.drawDataLabels();g&&a.animate(true);a.drawGraph&&a.drawGraph();a.drawPoints();a.options.enableMouseTracking!== +false&&a.drawTracker();g&&a.animate();setTimeout(function(){h.isAnimating=false;if((c=a.group)&&h!==b.clipRect&&h.renderer){c.clip(a.clipRect=b.clipRect);h.destroy()}},f);a.isDirty=false},redraw:function(){var a=this.chart,b=this.group;if(b){a.inverted&&b.attr({width:a.plotWidth,height:a.plotHeight});b.animate({translateX:a.plotLeft,translateY:a.plotTop})}this.translate();this.setTooltipPoints(true);this.render()},setState:function(a){var b=this.options,c=this.graph,d=b.states;b=b.lineWidth;a=a|| +ib;if(this.state!==a){this.state=a;if(!(d[a]&&d[a].enabled===false)){if(a)b=d[a].lineWidth||b+1;if(c&&!c.dashstyle)c.attr({"stroke-width":b},a?0:500)}}},setVisible:function(a,b){var c=this.chart,d=this.legendItem,e=this.group,f=this.tracker,g=this.dataLabelsGroup,h,i=this.data,j=c.options.chart.ignoreHiddenSeries;h=this.visible;h=(this.visible=a=a===Wa?!h:a)?"show":"hide";e&&e[h]();if(f)f[h]();else for(e=i.length;e--;){f=i[e];f.tracker&&f.tracker[h]()}g&&g[h]();d&&c.legend.colorizeItem(this,a);this.isDirty= +true;this.options.stacking&&u(c.series,function(m){if(m.options.stacking&&m.visible)m.isDirty=true});if(j)c.isDirtyBox=true;b!==false&&c.redraw();Pa(this,h)},show:function(){this.setVisible(true)},hide:function(){this.setVisible(false)},select:function(a){this.selected=a=a===Wa?!this.selected:a;if(this.checkbox)this.checkbox.checked=a;Pa(this,a?"select":"unselect")},drawTracker:function(){var a=this,b=a.options,c=[].concat(a.graphPath),d=c.length,e=a.chart,f=e.options.tooltip.snap,g=a.tracker,h=b.cursor; +h=h&&{cursor:h};var i=a.singlePoints,j;if(d)for(j=d+1;j--;){c[j]===Za&&c.splice(j+1,0,c[j+1]-f,c[j+2],Ka);if(j&&c[j]===Za||j===d)c.splice(j,0,Ka,c[j-2]+f,c[j-1])}for(j=0;ja&&i>e){i=Ia(a,e);m=2*e-i}else if(ig&&m>e){m=Ia(g,e);i=2*e-m}else if(m< +g&&my?Sa-y:M-(Z<=M?y:0)}Rb=Ea-3}sa(C,{barX:Na,barY:Ea,barW:U,barH:gb});C.shapeType="rect";Z=sa(b.renderer.Element.prototype.crisp.apply({}, +[e,Na,Ea,U,gb]),{r:c.borderRadius});if(e%2){Z.y-=1;Z.height+=1}C.shapeArgs=Z;C.trackerArgs=K(Rb)&&Ca(C.shapeArgs,{height:Ia(6,gb+3),y:Rb})})},getSymbol:function(){},drawGraph:function(){},drawPoints:function(){var a=this,b=a.options,c=a.chart.renderer,d,e;u(a.data,function(f){var g=f.plotY;if(g!==Wa&&!isNaN(g)&&f.y!==null){d=f.graphic;e=f.shapeArgs;if(d){Hc(d);d.animate(e)}else f.graphic=c[f.shapeType](e).attr(f.pointAttr[f.selected?"select":ib]).add(a.group).shadow(b.shadow)}})},drawTracker:function(){var a= +this,b=a.chart,c=b.renderer,d,e,f=+new Date,g=a.options,h=g.cursor,i=h&&{cursor:h},j;u(a.data,function(m){e=m.tracker;d=m.trackerArgs||m.shapeArgs;delete d.strokeWidth;if(m.y!==null)if(e)e.attr(d);else m.tracker=c[m.shapeType](d).attr({isTracker:f,fill:ce,visibility:a.visible?Ab:ob,zIndex:g.zIndex||1}).on(Kb?"touchstart":"mouseover",function(v){j=v.relatedTarget||v.fromElement;b.hoverSeries!==a&&Ga(j,"isTracker")!==f&&a.onMouseOver();m.onMouseOver()}).on("mouseout",function(v){if(!g.stickyTracking){j= +v.relatedTarget||v.toElement;Ga(j,"isTracker")!==f&&a.onMouseOut()}}).css(i).add(m.group||b.trackerGroup)})},animate:function(a){var b=this,c=b.data;if(!a){u(c,function(d){var e=d.graphic;d=d.shapeArgs;if(e){e.attr({height:0,y:b.yAxis.translate(0,0,1)});e.animate({height:d.height,y:d.y},b.options.animation)}});b.animate=null}},remove:function(){var a=this,b=a.chart;b.hasRendered&&u(b.series,function(c){if(c.type===a.type)c.isDirty=true});sb.prototype.remove.apply(a,arguments)}});wb.column=hd;ma=yb(hd, +{type:"bar",init:function(a){a.inverted=this.inverted=true;hd.prototype.init.apply(this,arguments)}});wb.bar=ma;ma=yb(sb,{type:"scatter",translate:function(){var a=this;sb.prototype.translate.apply(a);u(a.data,function(b){b.shapeType="circle";b.shapeArgs={x:b.plotX,y:b.plotY,r:a.chart.options.tooltip.snap}})},drawTracker:function(){var a=this,b=a.options.cursor,c=b&&{cursor:b},d;u(a.data,function(e){(d=e.graphic)&&d.attr({isTracker:true}).on("mouseover",function(){a.onMouseOver();e.onMouseOver()}).on("mouseout", +function(){a.options.stickyTracking||a.onMouseOut()}).css(c)})},cleanData:function(){}});wb.scatter=ma;ma=yb(Oc,{init:function(){Oc.prototype.init.apply(this,arguments);var a=this,b;sa(a,{visible:a.visible!==false,name:A(a.name,"Slice")});b=function(){a.slice()};Qa(a,"select",b);Qa(a,"unselect",b);return a},setVisible:function(a){var b=this.series.chart,c=this.tracker,d=this.dataLabel,e=this.connector,f=this.shadowGroup,g;g=(this.visible=a=a===Wa?!this.visible:a)?"show":"hide";this.group[g]();c&& +c[g]();d&&d[g]();e&&e[g]();f&&f[g]();this.legendItem&&b.legend.colorizeItem(this,a)},slice:function(a,b,c){var d=this.series.chart,e=this.slicedTranslation;oc(c,d);A(b,true);a=this.sliced=K(a)?a:!this.sliced;a={translateX:a?e[0]:d.plotLeft,translateY:a?e[1]:d.plotTop};this.group.animate(a);this.shadowGroup&&this.shadowGroup.animate(a)}});ma=yb(sb,{type:"pie",isCartesian:false,pointClass:ma,pointAttrToOptions:{stroke:"borderColor","stroke-width":"borderWidth",fill:"color"},getColor:function(){this.initialColor= +this.chart.counters.color},animate:function(){var a=this;u(a.data,function(b){var c=b.graphic;b=b.shapeArgs;var d=-kc/2;if(c){c.attr({r:0,start:d,end:d});c.animate({r:b.r,start:b.start,end:b.end},a.options.animation)}});a.animate=null},translate:function(){var a=0,b=-0.25,c=this.options,d=c.slicedOffset,e=d+c.borderWidth,f=c.center.concat([c.size,c.innerSize||0]),g=this.chart,h=g.plotWidth,i=g.plotHeight,j,m,v,P=this.data,T=2*kc,Y,H=tb(h,i),U,z,M,y=c.dataLabels.distance;f=tc(f,function(C,Z){return(U= +/%$/.test(C))?[h,i,H,H][Z]*ja(C)/100:C});this.getX=function(C,Z){v=Fa.asin((C-f[1])/(f[2]/2+y));return f[0]+(Z?-1:1)*rb(v)*(f[2]/2+y)};this.center=f;u(P,function(C){a+=C.y});u(P,function(C){Y=a?C.y/a:0;j=W(b*T*1E3)/1E3;b+=Y;m=W(b*T*1E3)/1E3;C.shapeType="arc";C.shapeArgs={x:f[0],y:f[1],r:f[2]/2,innerR:f[3]/2,start:j,end:m};v=(m+j)/2;C.slicedTranslation=tc([rb(v)*d+g.plotLeft,Cb(v)*d+g.plotTop],W);z=rb(v)*f[2]/2;M=Cb(v)*f[2]/2;C.tooltipPos=[f[0]+z*0.7,f[1]+M*0.7];C.labelPos=[f[0]+z+rb(v)*y,f[1]+M+Cb(v)* +y,f[0]+z+rb(v)*e,f[1]+M+Cb(v)*e,f[0]+z,f[1]+M,y<0?"center":v0,T=[[],[]],Y,H,U,z,M=2,y;if(d.enabled){sb.prototype.drawDataLabels.apply(this);u(a,function(gb){if(gb.dataLabel)T[gb.labelPos[7]U){h=[].concat(Sa);h.sort(z);for(y=Na;y--;)h[y].rank= +y;for(y=Na;y--;)Sa[y].rank>=U&&Sa.splice(y,1);Na=Sa.length}for(y=0;yH&&C[Ea+1]!==null||Y/g, '') + .split(//g), + childNodes = textNode.childNodes, + styleRegex = /style="([^"]+)"/, + hrefRegex = /href="([^"]+)"/, + parentX = attr(textNode, 'x'), + textStyles = wrapper.styles, + renderAsHtml = textStyles && wrapper.useHTML && !this.forExport, + htmlNode = wrapper.htmlNode, + //arr, issue #38 workaround + width = textStyles && pInt(textStyles.width), + textLineHeight = textStyles && textStyles.lineHeight, + lastLine, + GET_COMPUTED_STYLE = 'getComputedStyle', + 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, '') || ' ') + .replace(/</g, '<') + .replace(/>/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) { + + // allow getting the right offset height in exporting in IE + if (!hasSVG && wrapper.renderer.forExport) { + css(tspan, { display: 'block' }); + } + + // Webkit and opera sometimes return 'normal' as the line height. In that + // case, webkit uses offsetHeight, while Opera falls back to 18 + lineHeight = win[GET_COMPUTED_STYLE] && + pInt(win[GET_COMPUTED_STYLE](lastLine, null).getPropertyValue('line-height')); + + if (!lineHeight || 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, { + dy: textLineHeight || 16, + x: parentX + }); + 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()); + } + if (words.length) { + tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-'))); + } + } + } + } + }); + }); + + // Fix issue #38 and allow HTML in tooltips and other labels + if (renderAsHtml) { + if (!htmlNode) { + htmlNode = wrapper.htmlNode = createElement('span', null, extend(textStyles, { + position: ABSOLUTE, + top: 0, + left: 0 + }), this.box.parentNode); + } + htmlNode.innerHTML = wrapper.textStr; + + i = childNodes.length; + while (i--) { + childNodes[i].style.visibility = HIDDEN; + } + } + }, + + /** + * 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; + strokeWidth = x.strokeWidth; + 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) { + var elem = this.createElement('g'); + return defined(name) ? elem.attr({ 'class': PREFIX + name }) : elem; + }, + + /** + * 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 + if (elemWrapper.element.setAttributeNS) { + elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink', + 'href', src); + } else { + // could be exporting in IE + // using href throws "not supported" in ie7 and under, requries regex shim to fix later + elemWrapper.element.setAttribute('hc-svg-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( + mathRound(x), + mathRound(y), + radius, + options + ), + + imageRegex = /^url\((.*?)\)$/, + imageSrc, + imageSize; + + 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)) { + + var centerImage = function (img, size) { + img.attr({ + width: size[0], + height: size[1] + }).translate( + -mathRound(size[0] / 2), + -mathRound(size[1] / 2) + ); + }; + + imageSrc = symbol.match(imageRegex)[1]; + imageSize = symbolSizes[imageSrc]; + + // create the image synchronously, add attribs async + obj = this.image(imageSrc) + .attr({ + x: x, + y: y + }); + + if (imageSize) { + centerImage(obj, imageSize); + } else { + // initialize image to be 0 size so export will still function if there's no cached sizes + obj.attr({ width: 0, height: 0 }); + + // create a dummy JavaScript image to get the width and height + createElement('img', { + onload: function () { + var img = this; + centerImage(obj, symbolSizes[imageSrc] = [img.width, img.height]); + }, + 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; + wrapper.clipPath = clipPath; + + 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); + + // Keep a reference to the gradient object so it is possible to destroy it later + renderer.gradients.push(gradientObject); + + // The gradient needs to keep a list of stops to be able to destroy them + gradientObject.stops = []; + each(color.stops, function (stop) { + var stopObject; + if (regexRgba.test(stop[1])) { + colorObject = Color(stop[1]); + stopColor = colorObject.get('rgb'); + stopOpacity = colorObject.get('a'); + } else { + stopColor = stop[1]; + stopOpacity = 1; + } + stopObject = renderer.createElement('stop').attr({ + offset: stop[0], + 'stop-color': stopColor, + 'stop-opacity': stopOpacity + }).add(gradientObject); + + // Add the stop element to the gradient + gradientObject.stops.push(stopObject); + }); + + 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 { + // Remove the opacity attribute added above. Does not throw if the attribute is not there. + elem.removeAttribute(prop + '-opacity'); + + return color; + } + + }, + + + /** + * Add text to the SVG object + * @param {String} str + * @param {Number} x Left position + * @param {Number} y Top position + * @param {Boolean} useHTML Use HTML to render the text + */ + text: function (str, x, y, useHTML) { + + // 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({ + fontFamily: defaultChartStyle.fontFamily, + fontSize: defaultChartStyle.fontSize + }); + + wrapper.x = x; + wrapper.y = y; + wrapper.useHTML = useHTML; + return wrapper; + } +}; // end SVGRenderer + +// general renderer +Renderer = 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; + } else if (value[i] === 'Z') { // close the path + 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)) { + + this[key] = value; // used in getter + + // 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; + } else if (key === 'text') { // text for rotated and non-rotated elements + this.bBox = null; + 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; + }, + + /** + * Removes a child either by removeChild or move to garbageBin. + * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not. + */ + safeRemoveChild: function (element) { + // discardElement will detach the node from its parent before attaching it + // to the garbage bin. Therefore it is important that the node is attached and have parent. + var parentNode = element.parentNode; + if (parentNode) { + discardElement(element); + } + }, + + /** + * Extend element.destroy by removing it from the clip members array + */ + destroy: function () { + var wrapper = this; + + if (wrapper.destroyClip) { + wrapper.destroyClip(); + } + + return 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 wrapper = this, + element = wrapper.element, + bBox = wrapper.bBox; + + if (!bBox) { + // faking getBBox in exported SVG in legacy IE + if (element.nodeName === 'text') { + element.style.position = ABSOLUTE; + } + + bBox = wrapper.bBox = { + x: element.offsetLeft, + y: element.offsetTop, + width: element.offsetWidth, + height: element.offsetHeight + }; + } + return bBox; + + }, + + /** + * 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 () { + // 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, group) { + var shadows = [], + i, + element = this.element, + renderer = this.renderer, + shadow, + elemStyle = element.style, + markup, + path = element.path; + + // some times empty paths are not strings + if (path && typeof path.value !== 'string') { + 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 + if (group) { + group.element.appendChild(shadow); + } else { + 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 + + Element: VMLElement, + 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.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 { + var strokeNodes = elem.getElementsByTagName(prop); + if (strokeNodes.length) { + strokeNodes[0].opacity = 1; + } + 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; + strokeWidth = x.strokeWidth; + 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' + ]; + + } + } +}); + +// general renderer +Renderer = VMLRenderer; +} +/* **************************************************************************** + * * + * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE * + * * + *****************************************************************************/ + + +/** + * 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} options + */ + function Axis(userOptions) { + + // Define variables + var isXAxis = userOptions.isX, + opposite = userOptions.opposite, // needed in setOptions + horiz = inverted ? !isXAxis : isXAxis, + side = horiz ? + (opposite ? 0 : 2) : // top : bottom + (opposite ? 1 : 3), // right : left + stacks = {}, + + options = merge( + isXAxis ? defaultXAxisOptions : defaultYAxisOptions, + [defaultTopAxisOptions, defaultRightAxisOptions, + defaultBottomAxisOptions, defaultLeftAxisOptions][side], + userOptions + ), + + axis = this, + axisTitle, + type = options.type, + isDatetimeAxis = type === 'datetime', + isLog = type === 'logarithmic', + 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, + userMin, + userMax, + 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, + value = categories && defined(categories[pos]) ? categories[pos] : pos, + label = this.label; + + + // get the string + str = labelFormatter.call({ + isFirst: pos === tickPositions[0], + isLast: pos === tickPositions[tickPositions.length - 1], + dateTimeLabelFormat: dateTimeLabelFormat, + value: isLog ? lin2log(value) : value + }); + + + // prepare CSS + css = width && { width: mathMax(1, mathRound(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, + labelOptions.useHTML + ) + .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; + } + if (major) { + attribs.zIndex = 1; + } + tick.gridLine = gridLine = + gridLineWidth ? + renderer.path(gridLinePath) + .attr(attribs).add(gridGroup) : + null; + } + + // If the parameter 'old' is set, the current call will be followed + // by another call, therefore do not do any animations this time + if (!old && 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 && !isNaN(x)) { + 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 += pInt(label.styles.lineHeight) * 0.9 - label.getBBox().height / 2; + } + + + // correct for staggered labels + if (staggerLines) { + y += (index / (step || 1) % 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 () { + destroyObjectProperties(this); + } + }; + + /** + * 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, + from = options.from, + value = options.value, + toPath, // bands only + dashStyle = options.dashStyle, + svgElem = plotLine.svgElem, + path = [], + addEvent, + eventType, + xs, + ys, + x, + y, + color = options.color, + zIndex = options.zIndex, + events = options.events, + attribs; + + // logarithmic conversion + if (isLog) { + from = log2lin(from); + to = log2lin(to); + value = log2lin(value); + } + + // plot line + if (width) { + path = getPlotLinePath(value, width); + attribs = { + stroke: color, + 'stroke-width': width + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + } else if (defined(from) && defined(to)) { // plot band + // 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], pick(path[6], path[1])]; + ys = [path[2], path[5], pick(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; + + destroyObjectProperties(obj); + + // remove it from the lookup + erase(plotLinesAndBands, obj); + } + }; + + /** + * The class for stack items + */ + function StackItem(options, isNegative, x, stackOption) { + var stackItem = this; + + // Tells if the stack is negative + stackItem.isNegative = isNegative; + + // Save the options to be able to style the label + stackItem.options = options; + + // Save the x value to be able to position the label later + stackItem.x = x; + + // Save the stack option on the series configuration object + stackItem.stack = stackOption; + + // The align options and text align varies on whether the stack is negative and + // if the chart is inverted or not. + // First test the user supplied value, then use the dynamic. + stackItem.alignOptions = { + align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'), + verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')), + y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)), + x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0) + }; + + stackItem.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center'); + } + + StackItem.prototype = { + destroy: function () { + destroyObjectProperties(this); + }, + + /** + * Sets the total of this stack. Should be called when a serie is hidden or shown + * since that will affect the total of other stacks. + */ + setTotal: function (total) { + this.total = total; + this.cum = total; + }, + + /** + * Renders the stack total label and adds it to the stack label group. + */ + render: function (group) { + var stackItem = this, // aliased this + str = stackItem.options.formatter.call(stackItem); // format the text in the label + + // Change the text to reflect the new total and set visibility to hidden in case the serie is hidden + if (stackItem.label) { + stackItem.label.attr({text: str, visibility: HIDDEN}); + // Create new label + } else { + stackItem.label = + chart.renderer.text(str, 0, 0) // dummy positions, actual position updated with setOffset method in columnseries + .css(stackItem.options.style) // apply style + .attr({align: stackItem.textAlign, // fix the text-anchor + rotation: stackItem.options.rotation, // rotation + visibility: HIDDEN }) // hidden until setOffset is called + .add(group); // add to the labels-group + } + }, + + /** + * Sets the offset that the stack has from the x value and repositions the label. + */ + setOffset: function (xOffset, xWidth) { + var stackItem = this, // aliased this + neg = stackItem.isNegative, // special treatment is needed for negative stacks + y = axis.translate(stackItem.total), // stack value translated mapped to chart coordinates + yZero = axis.translate(0), // stack origin + h = mathAbs(y - yZero), // stack height + x = chart.xAxis[0].translate(stackItem.x) + xOffset, // stack x position + plotHeight = chart.plotHeight, + stackBox = { // this is the box for the complete stack + x: inverted ? (neg ? y : y - h) : x, + y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y), + width: inverted ? h : xWidth, + height: inverted ? xWidth : h + }; + + if (stackItem.label) { + stackItem.label + .align(stackItem.alignOptions, null, stackBox) // align the label to the box + .attr({visibility: VISIBLE}); // set visibility + } + } + }; + + /** + * 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, + stackOption, + negKey; + + if (!isXAxis) { + stacking = serie.options.stacking; + usePercentage = stacking === 'percent'; + + // create a stack for this particular series type + if (stacking) { + stackOption = serie.options.stack; + stackKey = serie.type + pick(stackOption, ''); + 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) { + 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; + } + } else if (defined(pointY)) { // y axis + 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] = {}; + } + + // If the StackItem is there, just update the values, + // if not, create one first + if (!stacks[key][pointX]) { + stacks[key][pointX] = new StackItem(options.stackLabels, isNegative, pointX, stackOption); + } + stacks[key][pointX].setTotal(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) { + var threshold = 0; // use series.options.threshold? + if (dataMin >= threshold) { + dataMin = threshold; + ignoreMinPadding = true; + } else if (dataMax < threshold) { + dataMax = threshold; + ignoreMaxPadding = true; + } + } + } + } + }); + + } + + /** + * Translate from axis value to pixel position on the chart, or back + * + */ + translate = function (val, backwards, cvsCoord, old, handleLog) { + 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 + if (isLog && handleLog) { + returnValue = lin2log(returnValue); + } + + } else { // normal translation + if (isLog && handleLog) { + val = log2lin(val); + } + 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, i; + + // 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 || isLog) { + if (magnitude === 1) { + multiples = [1, 2, 5, 10]; + } else if (magnitude <= 0.1) { + multiples = [1 / magnitude]; + } + } + } + + // normalize the interval to the nearest multiple + for (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; + magnitude = pick(magnitude, math.pow(10, mathFloor(math.log(tickInterval) / math.LN10))); + + if (magnitude < 1) { + invMag = mathRound(1 / magnitude) * 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 = correctFloat(mathFloor(min / tickInterval) * tickInterval), + roundedMax = correctFloat(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() { + var length, + catPad, + linkedParent, + linkedParentExtremes, + tickIntervalOption = options.tickInterval, + tickPixelIntervalOption = options.tickPixelInterval, + maxZoom = options.maxZoom || ( + isXAxis && !defined(options.min) && !defined(options.max) ? + 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); + } else { // initial min and max from the extreme data values + min = pick(userMin, options.min, dataMin); + max = pick(userMax, options.max, dataMax); + } + + if (isLog) { + min = log2lin(min); + max = log2lin(max); + } + + // 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(userMin) && minPadding && (dataMin < 0 || !ignoreMinPadding)) { + min -= length * minPadding; + } + if (!defined(options.max) && !defined(userMax) && 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, userMin))) { + min -= catPad; + } + if (categories || !defined(pick(options.max, userMax))) { + 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 + + userMin = newMin; + userMax = newMax; + + + // redraw + if (redraw) { + chart.redraw(animation); + } + }); + + } + + /** + * Get the actual axis extremes + */ + function getExtremes() { + return { + min: min, + max: max, + dataMin: dataMin, + dataMax: dataMax, + userMin: userMin, + userMax: userMax + }; + } + + /** + * 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], + n; + + 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 (n in ticks) { + ticks[n].destroy(); + delete ticks[n]; + } + } + + if (axisTitleOptions && axisTitleOptions.text) { + if (!axisTitle) { + axisTitle = axis.axisTitle = renderer.text( + axisTitleOptions.text, + 0, + 0, + axisTitleOptions.useHTML + ) + .attr({ + zIndex: 7, + rotation: axisTitleOptions.rotation || 0, + align: + axisTitleOptions.textAlign || + { low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align] + }) + .css(axisTitleOptions.style) + .add(); + axisTitle.isNew = true; + } + + titleOffset = 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, + stackLabelOptions = options.stackLabels, + 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 <= 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) { + var pos; + for (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 (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); + + axisTitle[axisTitle.isNew ? 'attr' : 'animate']({ + x: horiz ? + alongAxis : + offAxis + (opposite ? plotWidth : 0) + offset + + (axisTitleOptions.x || 0), // x + y: horiz ? + offAxis - (opposite ? plotHeight : 0) + offset : + alongAxis + (axisTitleOptions.y || 0) // y + }); + axisTitle.isNew = false; + } + + // Stacked totals: + if (stackLabelOptions && stackLabelOptions.enabled) { + var stackKey, oneStack, stackCategory, + stackTotalGroup = axis.stackTotalGroup; + + // Create a separate group for the stack total labels + if (!stackTotalGroup) { + axis.stackTotalGroup = stackTotalGroup = + renderer.g('stack-labels') + .attr({ + visibility: VISIBLE, + zIndex: 6 + }) + .translate(plotLeft, plotTop) + .add(); + } + + // Render each stack total + for (stackKey in stacks) { + oneStack = stacks[stackKey]; + for (stackCategory in oneStack) { + oneStack[stackCategory].render(stackTotalGroup); + } + } + } + // End stacked totals + + 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 = userOptions.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(); + } + } + + /** + * Destroys an Axis instance. + */ + function destroy() { + var stackKey; + + // Remove the events + removeEvent(axis); + + // Destroy each stack total + for (stackKey in stacks) { + destroyObjectProperties(stacks[stackKey]); + + stacks[stackKey] = null; + } + + // Destroy stack total group + if (axis.stackTotalGroup) { + axis.stackTotalGroup = axis.stackTotalGroup.destroy(); + } + + // Destroy collections + each([ticks, minorTicks, alternateBands, plotLinesAndBands], function (coll) { + destroyObjectProperties(coll); + }); + + // Destroy local variables + each([axisLine, axisGroup, gridGroup, axisTitle], function (obj) { + if (obj) { + obj.destroy(); + } + }); + axisLine = axisGroup = gridGroup = axisTitle = null; + } + + + // 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, + destroy: destroy + }); + + // register event listeners + for (eventType in events) { + addEvent(axis, eventType, events[eventType]); + } + + // set min and max + setScale(); + + } // end Axis + + + /** + * The toolbar object + */ + function Toolbar() { + var buttons = {}; + + /*jslint unparam: true*//* allow the unused param title until Toolbar rewrite*/ + 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; + } + } + /*jslint unparam: false*/ + + 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, options.useHTML) + .attr({ zIndex: 1 }) + .css(style) + .add(group); + + group.hide(); + + /** + * Destroy the tooltip and its elements. + */ + function destroy() { + each(crosshairs, function (crosshair) { + if (crosshair) { + crosshair.destroy(); + } + }); + + // Destroy and clear local variables + each([box, label, group], function (obj) { + if (obj) { + obj.destroy(); + } + }); + box = label = group = null; + } + + /** + * 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, + 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, + show, + bBox, + plotX, + plotY = 0, + textConfig = {}, + text, + pointConfig = [], + tooltipPos = point.tooltipPos, + formatter = options.formatter || defaultFormatter, + hoverPoints = chart.hoverPoints, + placedTooltipPoint; + + // 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) { + /*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(item.getLabelConfig()); + }); + + 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 = point.getLabelConfig(); + } + 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' + }); + + placedTooltipPoint = placeBox(boxWidth, boxHeight, plotLeft, plotTop, plotWidth, plotHeight, {x: x, y: y}); + + // do the move + move(mathRound(placedTooltipPoint.x - boxOffLeft), mathRound(placedTooltipPoint.y - boxOffLeft)); + } + + + // crosshairs + if (crosshairsOptions) { + crosshairsOptions = splat(crosshairsOptions); // [x, y] + + var path, + i = crosshairsOptions.length, + attribs, + axis; + + while (i--) { + axis = point.series[i ? 'yAxis' : 'xAxis']; + if (crosshairsOptions[i] && axis) { + 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, + destroy: destroy + }; + } + + /** + * The mouse tracker object + * @param {Object} options + */ + function MouseTracker(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, + pageZoomFix = isWebKit && + doc.width / doc.body.scrollWidth - + 1, // #224, #348 + chartPosLeft, + chartPosTop, + chartX, + chartY; + + // 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 || pageZoomFix) { // only Opera needs position on mouse move, see below + chartPosition = getPosition(container); + chartPosLeft = chartPosition.left; + chartPosTop = chartPosition.top; + } + + // chartX and chartY + if (isIE) { // IE including IE9 that has chartX but in a different meaning + chartX = e.x; + chartY = e.y; + } else { + if (ePos.layerX === UNDEFINED) { // Opera and iOS + chartX = ePos.pageX - chartPosLeft; + chartY = ePos.pageY - chartPosTop; + } else { + chartX = e.layerX; + chartY = e.layerY; + } + } + + // correct for page zoom bug in WebKit + if (pageZoomFix) { + chartX += mathRound((pageZoomFix + 1) * chartPosLeft - chartPosLeft); + chartY += mathRound((pageZoomFix + 1) * chartPosTop - chartPosTop); + } + + return extend(e, { + chartX: chartX, + chartY: chartY + }); + } + + /** + * 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) { + 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) { + var translate = axis.translate, + isXAxis = axis.isXAxis, + isHorizontal = inverted ? !isXAxis : isXAxis, + selectionMin = translate( + isHorizontal ? + selectionLeft : + plotHeight - selectionTop - selectionBox.height, + true, + 0, + 0, + 1 + ), + selectionMax = translate( + isHorizontal ? + selectionLeft + selectionBox.width : + plotHeight - selectionTop, + true, + 0, + 0, + 1 + ); + + 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); + + } + + /** + * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea. + */ + function hideTooltipOnMouseMove(e) { + var pageX = defined(e.pageX) ? e.pageX : e.page.x, // In mootools the event is wrapped and the page x/y position is named e.page.x + pageY = defined(e.pageX) ? e.pageY : e.page.y; // Ref: http://mootools.net/docs/core/Types/DOMEvent + + if (chartPosition && + !isInsidePlot(pageX - chartPosition.left - plotLeft, + pageY - chartPosition.top - plotTop)) { + resetTracker(); + } + } + + /** + * 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); + + // issue #295, dragging not always working in Firefox + if (!hasTouch && e.preventDefault) { + e.preventDefault(); + } + + // record the start position + 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); + + // cache chart position for issue #149 fix + if (!chartPosition) { + chartPosition = getPosition(container); + } + + // 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 + hasDragged = Math.sqrt( + Math.pow(mouseDownX - chartX, 2) + + Math.pow(mouseDownY - chartY, 2) + ); + if (hasDragged > 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: optionsChart.selectionMarkerFill || '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); + + // issue #149 workaround + // The mouseleave event above does not always fire. Whenever the mouse is moving + // outside the plotarea, hide the tooltip + addEvent(doc, 'mousemove', hideTooltipOnMouseMove); + + 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; + }; + + } + + /** + * Destroys the MouseTracker object and disconnects DOM events. + */ + function destroy() { + // Destroy the tracker group element + if (chart.trackerGroup) { + chart.trackerGroup = trackerGroup = chart.trackerGroup.destroy(); + } + + removeEvent(doc, 'mousemove', hideTooltipOnMouseMove); + container.onclick = container.onmousedown = container.onmousemove = container.ontouchstart = container.ontouchend = container.ontouchmove = null; + } + + /** + * 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, + destroy: destroy + }); + } + + + + /** + * The overview of the chart's series + */ + var Legend = function () { + + 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), + 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, + lineColor = visible ? item.color : hiddenColor, + symbolAttr = visible ? item.pointAttr[NORMAL_STATE] : { + stroke: hiddenColor, + fill: hiddenColor + }; + + if (legendItem) { + legendItem.css({ fill: textColor }); + } + if (legendLine) { + legendLine.attr({ stroke: lineColor }); + } + if (legendSymbol) { + legendSymbol.attr(symbolAttr); + } + + } + + /** + * 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); + } + + + } + + /** + * Destroys the legend. + */ + function destroy() { + if (box) { + box = box.destroy(); + } + + if (legendGroup) { + legendGroup = legendGroup.destroy(); + } + } + + /** + * Position the checkboxes after the width is determined + */ + function positionCheckboxes() { + each(allItems, function (item) { + var checkbox = item.checkbox, + alignAttr = legendGroup.alignAttr; + if (checkbox) { + css(checkbox, { + left: (alignAttr.translateX + item.legendItemWidth + checkbox.x - 40) + PX, + top: (alignAttr.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, + simpleSymbol, + li = item.legendItem, + series = item.series || item, + itemOptions = series.options, + strokeWidth = (itemOptions && itemOptions.borderWidth) || 0; + + 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 () { + 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 && itemOptions && itemOptions.lineWidth) { + var attrs = { + 'stroke-width': itemOptions.lineWidth, + zIndex: 2 + }; + if (itemOptions.dashStyle) { + attrs.dashstyle = itemOptions.dashStyle; + } + item.legendLine = renderer.path([ + M, + -symbolWidth - symbolPadding, + 0, + L, + -symbolPadding, + 0 + ]) + .attr(attrs) + .add(legendGroup); + } + + // draw a simple symbol + if (simpleSymbol) { // bar|pie|area|column + + legendSymbol = renderer.rect( + (symbolX = -symbolWidth - symbolPadding), + (symbolY = -11), + symbolWidth, + 12, + 2 + ).attr({ + //'stroke-width': 0, + zIndex: 3 + }).add(legendGroup); + + // draw the marker + } else if (itemOptions && itemOptions.marker && itemOptions.marker.enabled) { + legendSymbol = renderer.symbol( + item.symbol, + (symbolX = -symbolWidth / 2 - symbolPadding), + (symbolY = -4), + itemOptions.marker.radius + ) + //.attr(item.pointAttr[NORMAL_STATE]) + .attr({ zIndex: 3 }) + .add(legendGroup); + + } + if (legendSymbol) { + legendSymbol.xOff = symbolX + (strokeWidth % 2 / 2); + legendSymbol.yOff = symbolY + (strokeWidth % 2 / 2); + } + + item.legendSymbol = legendSymbol; + + // colorize the items + colorizeItem(item, item.visible); + + + // add the HTML checkbox on top + if (itemOptions && itemOptions.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 + padding; + 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; + + if (!legendGroup) { + legendGroup = renderer.g('legend') + .attr({ zIndex: 7 }) + .add(); + } + + + // add each series or point + allItems = []; + each(series, function (serie) { + var seriesOptions = serie.options; + + if (!seriesOptions.showInLegend) { + return; + } + + // use points or series for the legend item depending on legendType + allItems = allItems.concat(seriesOptions.legendType === 'point' ? + serie.data : + serie + ); + + }); + + // sort by legendIndex + stableSort(allItems, function (a, b) { + return (a.options.legendIndex || 0) - (b.options.legendIndex || 0); + }); + + // reversed legend + if (reversedLegend) { + allItems.reverse(); + } + + // render the items + each(allItems, renderItem); + + + + // 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); + box.isNew = true; + + } else if (legendWidth > 0 && legendHeight > 0) { + box[box.isNew ? 'attr' : 'animate']( + box.crisp(null, null, null, legendWidth, legendHeight) + ); + box.isNew = false; + } + + // 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); + } + } + + if (allItems.length) { + 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, + destroy: destroy + }; + }; + + + + + + + /** + * 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(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 { // else, zoom in on all axes + 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 = title.destroy(); // remove old + } + if (chartTitleOptions && chartTitleOptions.text && !title) { + chart[name] = renderer.text( + chartTitleOptions.text, + 0, + 0, + chartTitleOptions.useHTML + ) + .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 - pInt(rect.left))) + PX, + top: (-(rect.top - pInt(rect.top))) + 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(win, 'resize', reflow); + addEvent(chart, 'destroy', function () { + removeEvent(win, 'resize', reflow); + }); + } + + /** + * Fires endResize event on chart instance. + */ + function fireEndResize() { + fireEvent(chart, 'endResize', null, function () { + isResizing -= 1; + }); + } + + /** + * 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; + chart.chartWidth = chartWidth = mathRound(width); + chart.chartHeight = 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 + // If animation is disabled, fire without delay + if (globalAnimation === false) { + fireEndResize(); + } else { // else set a timeout with the animation duration + setTimeout(fireEndResize, (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(); + + // 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(); + } + + // Credits + if (credits.enabled && !chart.credits) { + creditsHref = credits.href; + chart.credits = 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, + parentNode = container && container.parentNode; + + // If the chart is destroyed already, do nothing. + // This will happen if if a script invokes chart.destroy and + // then it will be called again on win.unload + if (chart === null) { + return; + } + + // fire the chart.destoy event + fireEvent(chart, 'destroy'); + + // remove events + removeEvent(win, 'unload', destroy); + removeEvent(chart); + + // ==== Destroy collections: + // Destroy axes + i = axes.length; + while (i--) { + axes[i] = axes[i].destroy(); + } + + // Destroy each series + i = series.length; + while (i--) { + series[i] = series[i].destroy(); + } + + // ==== Destroy chart properties: + each(['title', 'subtitle', 'seriesGroup', 'clipRect', 'credits', 'tracker'], function (name) { + var prop = chart[name]; + + if (prop) { + chart[name] = prop.destroy(); + } + }); + + // ==== Destroy local variables: + each([chartBackground, plotBorder, plotBackground, legend, tooltip, renderer, tracker], function (obj) { + if (obj && obj.destroy) { + obj.destroy(); + } + }); + chartBackground = plotBorder = plotBackground = legend = tooltip = renderer = tracker = null; + + // remove container and all SVG + if (container) { // can break in IE when destroyed before finished loading + container.innerHTML = ''; + removeEvent(container); + if (parentNode) { + discardElement(container); + } + + // IE6 leak + container = null; + } + + // memory and CPU leak + clearInterval(tooltipInterval); + + // clean it all up + for (i in chart) { + delete chart[i]; + } + + chart = null; + } + /** + * 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', + COMPLETE = 'complete'; + // Note: in spite of JSLint's complaints, win == win.top is required + /*jslint eqeq: true*/ + if (!hasSVG && win == win.top && doc.readyState !== COMPLETE) { + /*jslint eqeq: false*/ + doc.attachEvent(ONREADYSTATECHANGE, function () { + doc.detachEvent(ONREADYSTATECHANGE, firstRender); + if (doc.readyState === COMPLETE) { + 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(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 + + + // 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; + chart.counters = new ChartCounters(); + /* + 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, + counters = series.chart.counters, + 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[counters.color++]; + + // loop back to zero + counters.wrapColor(defaultColors.length); + } + + 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; + } else if (isObject(options) && !isNumber(options.length)) { // object input + // copy options directly to point + extend(point, options); + point.options = options; + } else if (isString(options[0])) { // categorized data with name in first position + point.name = options[0]; + point.y = options[1]; + } else if (isNumber(options[0])) { // two-dimentional array + 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, + hoverPoints = series.chart.hoverPoints, + prop; + + series.chart.pointCount--; + + if (hoverPoints) { + point.setState(); + erase(hoverPoints, point); + } + if (point === series.chart.hoverPoint) { + point.onMouseOut(); + } + + + // remove all events + removeEvent(point); + + each(['graphic', 'tracker', 'group', 'dataLabel', 'connector', 'shadowGroup'], 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; + } + + + }, + + /** + * Return the configuration hash needed for the data label and tooltip formatters + */ + getLabelConfig: function () { + var point = this; + return { + x: point.category, + y: point.y, + series: point.series, + point: point, + percentage: point.percentage, + total: point.total || point.stackTotal + }; + }, + + /** + * 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; + + selected = pick(selected, !point.selected); + + // fire the event with the defalut handler + point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () { + point.selected = selected; + 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(''); + + }, + + /** + * 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, + 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); + + // 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; + + 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]); + } else { + // if a graphic is not applied to each point in the normal state, create a shared + // graphic for the hover state + 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 + stableSort(data, 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[i - 1].destroy(); + data.splice(i - 1, 1); // remove the duplicate + } + } + }*/ + + // connect nulls + if (series.options.connectNulls) { + for (i = data.length - 1; i >= 0; i--) { + if (data[i].y === null && data[i - 1] && data[i + 1]) { + data.splice(i, 1); + } + } + } + + // 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 (interval > 0 && (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, + counters = this.chart.counters; + this.color = this.options.color || defaultColors[counters.color++] || '#0000ff'; + counters.wrapColor(defaultColors.length); + }, + /** + * Get the series' symbol + */ + getSymbol: function () { + var defaultSymbols = this.chart.options.symbols, + counters = this.chart.counters; + this.symbol = this.options.marker.symbol || defaultSymbols[counters.symbol++]; + counters.wrapSymbol(defaultSymbols.length); + }, + + /** + * 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); + } + series.getAttribs(); + + + // 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 + chart.counters.color = 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(); + + + // cache attributes for shapes + series.getAttribs(); + + // 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, 0, 1); + } + + // set the y value + if (yValue !== null) { + point.plotY = yAxis.translate(yValue, 0, 1, 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, + key; + + // 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 (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, + seriesClipRect = series.clipRect, + //chartSeries = series.chart.series, + issue134 = /\/5[0-9\.]+ (Safari|Mobile)\//.test(userAgent), // todo: update when Safari bug is fixed + destroy, + prop; + + // add event hook + fireEvent(series, 'destroy'); + + // 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(); + }); + + // If this series clipRect is not the global one (which is removed on chart.destroy) we + // destroy it here. + if (seriesClipRect && seriesClipRect !== chart.clipRect) { + series.clipRect = seriesClipRect.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, + seriesOptions = series.options, + options = seriesOptions.dataLabels, + str, + dataLabelsGroup = series.dataLabelsGroup, + chart = series.chart, + renderer = chart.renderer, + inverted = chart.inverted, + seriesType = series.type, + color, + stacking = seriesOptions.stacking, + isBarLike = seriesType === 'column' || seriesType === 'bar', + vAlignIsNull = options.verticalAlign === null, + yIsNull = options.y === null; + + if (isBarLike) { + if (stacking) { + // In stacked series the default label placement is inside the bars + if (vAlignIsNull) { + options = merge(options, {verticalAlign: 'middle'}); + } + + // If no y delta is specified, try to create a good default + if (yIsNull) { + options = merge(options, {y: {top: 14, middle: 4, bottom: -6}[options.verticalAlign]}); + } + } else { + // In non stacked series the default label placement is on top of the bars + if (vAlignIsNull) { + options = merge(options, {verticalAlign: 'top'}); + } + } + } + + // create a separate group for the data labels to avoid rotation + if (!dataLabelsGroup) { + dataLabelsGroup = series.dataLabelsGroup = + renderer.g('data-labels') + .attr({ + visibility: series.visible ? VISIBLE : HIDDEN, + zIndex: 6 + }) + .translate(chart.plotLeft, chart.plotTop) + .add(); + } else { + dataLabelsGroup.translate(chart.plotLeft, chart.plotTop); + } + + // determine the color + color = options.color; + if (color === 'auto') { // 1.0 backwards compatibility + color = null; + } + options.style.color = pick(color, series.color, 'black'); + + // make the labels for each point + each(data, function (point) { + var barX = point.barX, + plotX = (barX && barX + point.barW / 2) || point.plotX || -999, + plotY = pick(point.plotY, -999), + dataLabel = point.dataLabel, + align = options.align, + individualYDelta = yIsNull ? (point.y >= 0 ? -6 : 12) : options.y; + + // get the string + str = options.formatter.call(point.getLabelConfig()); + x = (inverted ? chart.plotWidth - plotY : plotX) + options.x; + y = (inverted ? chart.plotHeight - plotX : plotY) + individualYDelta; + + // in columns, align the string to the column + if (seriesType === 'column') { + x += { left: -1, right: 1 }[align] * point.barW / 2 || 0; + } + + if (inverted && point.y < 0) { + align = 'right'; + x -= 10; + } + + // update existing label + if (dataLabel) { + // vertically centered + if (inverted && !options.y) { + y = y + pInt(dataLabel.styles.lineHeight) * 0.9 - dataLabel.getBBox().height / 2; + } + dataLabel + .attr({ + text: str + }).animate({ + x: x, + y: y + }); + // create new label + } else if (defined(str)) { + dataLabel = point.dataLabel = 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 + pInt(dataLabel.styles.lineHeight) * 0.9 - dataLabel.getBBox().height / 2 + }); + } + } + + + /*if (series.isCartesian) { + dataLabel[chart.isInsidePlot(plotX, plotY) ? 'show' : 'hide'](); + }*/ + + if (isBarLike && seriesOptions.stacking && dataLabel) { + var barY = point.barY, + barW = point.barW, + barH = point.barH; + + dataLabel.align(options, null, + { + x: inverted ? chart.plotWidth - barY - barH : barX, + y: inverted ? chart.plotHeight - barX - barW : barY, + width: inverted ? barH : barW, + height: inverted ? barW : barH + }); + } + }); + } + }, + + /** + * Draw the actual graph + */ + drawGraph: function () { + 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) { + stop(graph); // cancel running animations, #459 + 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 + addEvent(series, 'destroy', function () { + removeEvent(chart, 'resize', setInvert); + }); + } + 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, + 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: options.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]; + } else { // curve from last point to this + 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, + options = series.options, + stacking = options.stacking, + borderWidth = options.borderWidth, + 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 && otherSeries.visible) { + if (otherSeries.options.stacking) { + stackKey = otherSeries.stackKey; + if (stackGroups[stackKey] === UNDEFINED) { + stackGroups[stackKey] = columnCount++; + } + columnIndex = stackGroups[stackKey]; + } else { + 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 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), + stack = series.yAxis.stacks[(point.y < 0 ? '-' : '') + series.stackKey], + trackerY, + shapeArgs; + + // Record the offset'ed position and width of the bar to be able to align the stacking total correctly + if (stacking && series.visible && stack && stack[point.x]) { + stack[point.x].setOffset(pointXOffset, pointWidth); + } + + // 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 + }); + + // create shape type and shape args that are reused in drawPoints and drawTracker + point.shapeType = 'rect'; + shapeArgs = extend(chart.renderer.Element.prototype.crisp.apply({}, [ + borderWidth, + barX, + barY, + pointWidth, + barH + ]), { + r: options.borderRadius + }); + if (borderWidth % 2) { // correct for shorting in crisp method, visible in stacked columns with 1px border + shapeArgs.y -= 1; + shapeArgs.height += 1; + } + point.shapeArgs = shapeArgs; + + // 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) && point.y !== null) { + 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(), + options = series.options, + cursor = options.cursor, + css = cursor && { cursor: cursor }, + rel; + + each(series.data, function (point) { + tracker = point.tracker; + shapeArgs = point.trackerArgs || point.shapeArgs; + delete shapeArgs.strokeWidth; + 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: options.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 (!options.stickyTracking) { + rel = event.relatedTarget || event.toElement; + if (attr(rel, 'isTracker') !== trackerLabel) { + series.onMouseOut(); + } + } + }) + .css(css) + .add(point.group || chart.trackerGroup); // pies have point group - see issue #118 + } + } + }); + }, + + + /** + * 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, + shapeArgs = point.shapeArgs; + + if (graphic) { + // start values + graphic.attr({ + height: 0, + y: series.yAxis.translate(0, 0, 1) + }); + + // animate + graphic.animate({ + height: shapeArgs.height, + y: shapeArgs.y + }, 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 () { + series.onMouseOver(); + point.onMouseOver(); + }) + .on('mouseout', function () { + 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, + shadowGroup = point.shadowGroup, + 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 (shadowGroup) { + shadowGroup[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, + translation; + + 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; + + translation = { + translateX: (sliced ? slicedTranslation[0] : chart.plotLeft), + translateY: (sliced ? slicedTranslation[1] : chart.plotTop) + }; + point.group.animate(translation); + if (point.shadowGroup) { + point.shadowGroup.animate(translation); + } + + } +}); + +/** + * 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 = this.chart.counters.color; + }, + + /** + * Animate the column heights one by one from zero + */ + animate: function () { + 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.concat([options.size, options.innerSize || 0]), + 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 = 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 + // i == 4: innerSize, 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, + shadow = series.options.shadow, + shadowGroup, + shapeArgs; + + + // draw the slices + each(series.data, function (point) { + graphic = point.graphic; + shapeArgs = point.shapeArgs; + group = point.group; + shadowGroup = point.shadowGroup; + + // put the shadow behind all points + if (shadow && !shadowGroup) { + shadowGroup = point.shadowGroup = renderer.g('shadow') + .attr({ zIndex: 4 }) + .add(); + } + + // 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]); + if (shadowGroup) { + shadowGroup.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) + .shadow(shadow, shadowGroup); + } + + // 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, + softConnector = pick(options.softConnector, true), + distanceOption = options.distance, + seriesCenter = series.center, + radius = seriesCenter[2] / 2, + centerY = seriesCenter[1], + outside = distanceOption > 0, + dataLabel, + labelPos, + labelHeight, + halves = [// divide the points into right and left halves for anti collision + [], // right + [] // left + ], + x, + y, + visibility, + rankArr, + sort, + i = 2, + j; + + // get out if not enabled + if (!options.enabled) { + return; + } + + // run parent method + Series.prototype.drawDataLabels.apply(series); + + // arrange points for detection collision + each(data, function (point) { + if (point.dataLabel) { // it may have been cancelled in the base method (#407) + halves[ + point.labelPos[7] < mathPI / 2 ? 0 : 1 + ].push(point); + } + }); + halves[1].reverse(); + + // define the sorting algorithm + sort = function (a, b) { + return b.y - a.y; + }; + + // assume equal label heights + labelHeight = halves[0][0] && halves[0][0].dataLabel && pInt(halves[0][0].dataLabel.styles.lineHeight); + + /* Loop over the points in each quartile, starting from the top and bottom + * of the pie to detect overlapping labels. + */ + while (i--) { + + var slots = [], + slotsLength, + usedSlots = [], + points = halves[i], + pos, + length = points.length, + slotIndex; + + + // build the slots + for (pos = centerY - radius - distanceOption; pos <= centerY + radius + distanceOption; pos += labelHeight) { + slots.push(pos); + // visualize the slot + /* + var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0), + slotY = pos + chart.plotTop; + if (!isNaN(slotX)) { + chart.renderer.rect(slotX, slotY - 7, 100, labelHeight) + .attr({ + 'stroke-width': 1, + stroke: 'silver' + }) + .add(); + chart.renderer.text('Slot '+ (slots.length - 1), slotX, slotY + 4) + .attr({ + fill: 'silver' + }).add(); + } + // */ + } + slotsLength = slots.length; + + // if there are more values than available slots, remove lowest values + if (length > slotsLength) { + // create an array for sorting and ranking the points within each quarter + rankArr = [].concat(points); + rankArr.sort(sort); + j = length; + while (j--) { + rankArr[j].rank = j; + } + j = length; + while (j--) { + if (points[j].rank >= slotsLength) { + points.splice(j, 1); + } + } + length = points.length; + } + + // The label goes to the nearest open slot, but not closer to the edge than + // the label's index. + for (j = 0; j < length; j++) { + + point = points[j]; + labelPos = point.labelPos; + + var closest = 9999, + distance, + slotI; + + // find the closest slot index + for (slotI = 0; slotI < slotsLength; slotI++) { + distance = mathAbs(slots[slotI] - labelPos[1]); + if (distance < closest) { + closest = distance; + slotIndex = slotI; + } + } + + // if that slot index is closer to the edges of the slots, move it + // to the closest appropriate slot + if (slotIndex < j && slots[j] !== null) { // cluster at the top + slotIndex = j; + } else if (slotsLength < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom + slotIndex = slotsLength - length + j; + while (slots[slotIndex] === null) { // make sure it is not taken + slotIndex++; + } + } else { + // Slot is taken, find next free slot below. In the next run, the next slice will find the + // slot above these, because it is the closest one + while (slots[slotIndex] === null) { // make sure it is not taken + slotIndex++; + } + } + + usedSlots.push({ i: slotIndex, y: slots[slotIndex] }); + slots[slotIndex] = null; // mark as taken + } + // sort them in order to fill in from the top + usedSlots.sort(sort); + + + // now the used slots are sorted, fill them up sequentially + for (j = 0; j < length; j++) { + + point = points[j]; + labelPos = point.labelPos; + dataLabel = point.dataLabel; + var slot = usedSlots.pop(), + naturalY = labelPos[1]; + + visibility = point.visible === false ? HIDDEN : VISIBLE; + slotIndex = slot.i; + + // if the slot next to currrent slot is free, the y value is allowed + // to fall back to the natural position + y = slot.y; + if ((naturalY > y && slots[slotIndex + 1] !== null) || + (naturalY < y && slots[slotIndex - 1] !== null)) { + y = naturalY; + } + + // get the x - use the natural x position for first and last slot, to prevent the top + // and botton slice connectors from touching each other on either side + x = series.getX(slotIndex === 0 || slotIndex === slots.length - 1 ? naturalY : y, i); + + // 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 = softConnector ? [ + M, + x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label + 'C', + x, y, // first break, next to the label + 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5], + labelPos[2], labelPos[3], // second break + L, + labelPos[4], labelPos[5] // base + ] : [ + M, + x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at 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 || point.color || '#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, + hasRtlBug: hasRtlBug, + numberFormat: numberFormat, + Point: Point, + Color: Color, + Renderer: Renderer, + seriesTypes: seriesTypes, + setOptions: setOptions, + Series: Series, + + // Expose utility funcitons for modules + addEvent: addEvent, + removeEvent: removeEvent, + createElement: createElement, + discardElement: discardElement, + css: css, + each: each, + extend: extend, + map: map, + merge: merge, + pick: pick, + extendClass: extendClass, + product: 'Highcharts', + version: '2.1.9' +}; +}()); diff --git a/java/com.sap.sailing.gwt.ui/highcharts/js/modules/exporting.js b/java/com.sap.sailing.gwt.ui/highcharts/js/modules/exporting.js new file mode 100644 index 00000000000..76d3efb5d87 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/highcharts/js/modules/exporting.js @@ -0,0 +1,24 @@ +/* + Highcharts JS v2.1.9 (2011-11-11) + Exporting module + + (c) 2010-2011 Torstein H?nsi + + License: www.highcharts.com/license +*/ +(function(){var n=Highcharts,A=n.Chart,B=n.addEvent,E=n.removeEvent,t=n.createElement,w=n.discardElement,u=n.css,x=n.merge,q=n.each,r=n.extend,F=Math.max,s=document,G=window,C=s.documentElement.ontouchstart!==undefined,y=n.getOptions();r(y.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"});y.navigation={menuStyle:{border:"1px solid #A0A0A0", +background:"#FFFFFF"},menuItemStyle:{padding:"0 5px",background:"none",color:"#303030",fontSize:C?"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,height:20,hoverBorderColor:"#909090",hoverSymbolFill:"#81A7CF",hoverSymbolStroke:"#4572A5",symbolFill:"#E0E0E0",symbolStroke:"#A0A0A0",symbolX:11.5,symbolY:10.5,verticalAlign:"top", +width:24,y:10}};y.exporting={type:"image/png",url:"http://export.highcharts.com/",width:800,enableImages:false,buttons:{exportButton:{symbol:"exportIcon",x:-10,symbolFill:"#A8BF77",hoverSymbolFill:"#768F3E",_id:"exportButton",_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"})}}]},printButton:{symbol:"printIcon",x:-36,symbolFill:"#B5C9DF",hoverSymbolFill:"#779ABF",_id:"printButton",_titleKey:"printButtonTitle",onclick:function(){this.print()}}}};r(A.prototype,{getSVG:function(b){var c=this,a,f,d,k,e,j,h=x(c.options,b);if(!s.createElementNS)s.createElementNS=function(i,g){var o=s.createElement(g);o.getBBox=function(){return n.Renderer.prototype.Element.prototype.getBBox.apply({element:o})};return o};b=t("div",null, +{position:"absolute",top:"-9999em",width:c.chartWidth+"px",height:c.chartHeight+"px"},s.body);r(h.chart,{renderTo:b,forExport:true});h.exporting.enabled=false;if(!h.exporting.enableImages)h.chart.plotBackgroundImage=null;h.series=[];q(c.series,function(i){d=i.options;d.animation=false;d.showCheckbox=false;d.visible=i.visible;if(!h.exporting.enableImages)if(d&&d.marker&&/^url\(/.test(d.marker.symbol))d.marker.symbol="circle";d.data=[];q(i.data,function(g){k=g.config;e={x:g.x,y:g.y,name:g.name};typeof k=== +"object"&&g.config&&k.constructor!==Array&&r(e,k);e.visible=g.visible;d.data.push(e);if(!h.exporting.enableImages)(j=g.config&&g.config.marker)&&/^url\(/.test(j.symbol)&&delete j.symbol});h.series.push(d)});a=new Highcharts.Chart(h);q(["xAxis","yAxis"],function(i){q(c[i],function(g,o){var l=a[i][o],m=g.getExtremes(),p=m.userMin;m=m.userMax;if(p!==void 0||m!==void 0)l.setExtremes(p,m,true,false)})});f=a.container.innerHTML;h=null;a.destroy();w(b);f=f.replace(/zIndex="[^"]+"/g,"").replace(/isShadow="[^"]+"/g, +"").replace(/symbolName="[^"]+"/g,"").replace(/jQuery[0-9]+="[^"]+"/g,"").replace(/isTracker="[^"]+"/g,"").replace(/url\([^#]+#/g,"url(#").replace(/]+)/g,'id="$1"').replace(/class=([^" ]+)/g,'class="$1"').replace(/ transform /g," ").replace(/:(path|rect)/g,"$1").replace(/]*)>/gi,"").replace(/<\/image>/g,"").replace(/]*)([^\/])>/gi, +"").replace(/width=(\d+)/g,'width="$1"').replace(/height=(\d+)/g,'height="$1"').replace(/hc-svg-href="/g,'xlink:href="').replace(/style="([^"]+)"/g,function(i){return i.toLowerCase()});f=f.replace(/(url\(#highcharts-[0-9]+)"/g,"$1").replace(/"/g,"'");if(f.match(/ xmlns="/g).length===2)f=f.replace(/xmlns="[^"]+"/,"");return f},exportChart:function(b,c){var a,f=this.getSVG(c);b=x(this.options.exporting,b);a=t("form",{method:"post",action:b.url},{display:"none"},s.body);q(["filename", +"type","width","svg"],function(d){t("input",{type:"hidden",name:d,value:{filename:b.filename||"chart",type:b.type,width:b.width,svg:f}[d]},null,a)});a.submit();w(a)},print:function(){var b=this,c=b.container,a=[],f=c.parentNode,d=s.body,k=d.childNodes;if(!b.isPrinting){b.isPrinting=true;q(k,function(e,j){if(e.nodeType===1){a[j]=e.style.display;e.style.display="none"}});d.appendChild(c);G.print();setTimeout(function(){f.appendChild(c);q(k,function(e,j){if(e.nodeType===1)e.style.display=a[j]});b.isPrinting= +false},1E3)}},contextMenu:function(b,c,a,f,d,k){var e=this,j=e.options.navigation,h=j.menuItemStyle,i=e.chartWidth,g=e.chartHeight,o="cache-"+b,l=e[o],m=F(d,k),p,z;if(!l){e[o]=l=t("div",{className:"highcharts-"+b},{position:"absolute",zIndex:1E3,padding:m+"px"},e.container);p=t("div",null,r({MozBoxShadow:"3px 3px 10px #888",WebkitBoxShadow:"3px 3px 10px #888",boxShadow:"3px 3px 10px #888"},j.menuStyle),l);z=function(){u(l,{display:"none"})};B(l,"mouseleave",z);q(c,function(v){if(v){var D=t("div", +{onmouseover:function(){u(this,j.menuItemHoverStyle)},onmouseout:function(){u(this,h)},innerHTML:v.text||e.options.lang[v.textKey]},r({cursor:"pointer"},h),p);D[C?"ontouchstart":"onclick"]=function(){z();v.onclick.apply(e,arguments)};e.exportDivElements.push(D)}});e.exportDivElements.push(p,l);e.exportMenuWidth=l.offsetWidth;e.exportMenuHeight=l.offsetHeight}b={display:"block"};if(a+e.exportMenuWidth>i)b.right=i-a-d-m+"px";else b.left=a-m+"px";if(f+k+e.exportMenuHeight>g)b.bottom=g-f-m+"px";else b.top= +f+k-m+"px";u(l,b)},addButton:function(b){function c(){g.attr(m);i.attr(l)}var a=this,f=a.renderer,d=x(a.options.navigation.buttonOptions,b),k=d.onclick,e=d.menuItems,j=d.width,h=d.height,i,g,o;b=d.borderWidth;var l={stroke:d.borderColor},m={stroke:d.symbolStroke,fill:d.symbolFill};if(!a.exportDivElements){a.exportDivElements=[];a.exportSVGElements=[]}if(d.enabled!==false){i=f.rect(0,0,j,h,d.borderRadius,b).align(d,true).attr(r({fill:d.backgroundColor,"stroke-width":b,zIndex:19},l)).add();o=f.rect(0, +0,j,h,0).align(d).attr({id:d._id,fill:"rgba(255, 255, 255, 0.001)",title:a.options.lang[d._titleKey],zIndex:21}).css({cursor:"pointer"}).on("mouseover",function(){g.attr({stroke:d.hoverSymbolStroke,fill:d.hoverSymbolFill});i.attr({stroke:d.hoverBorderColor})}).on("mouseout",c).on("click",c).add();if(e)k=function(){c();var p=o.getBBox();a.contextMenu("export-menu",e,p.x,p.y,j,h)};o.on("click",function(){k.apply(a,arguments)});g=f.symbol(d.symbol,d.symbolX,d.symbolY,(d.symbolSize||12)/2).align(d,true).attr(r(m, +{"stroke-width":d.symbolStrokeWidth||1,zIndex:20})).add();a.exportSVGElements.push(i,o,g)}},destroyExport:function(){var b,c;for(b=0;b