From 7526d2891d06732215c512630e2f4a9a590efa79 Mon Sep 17 00:00:00 2001 From: Antoine Angenieux Date: Wed, 10 Oct 2012 17:05:55 +0200 Subject: [PATCH 01/15] [fork] change version for fork --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 09c335d..04e7704 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ nl.topicus whighcharts jar - 6.0-SNAPSHOT + 6.0-aangenieux-SNAPSHOT WHighCharts WiQuery-HighCharts bindings From 59a13f899f744547e161205d4e07e11459339c7f Mon Sep 17 00:00:00 2001 From: Antoine Angenieux Date: Wed, 10 Oct 2012 17:06:28 +0200 Subject: [PATCH 02/15] [eclipse conf] add encoding to other folders --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2061c9e..52978a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ target +.settings/org.eclipse.m2e.core.prefs .classpath .project From 241d6c49159c0de42d74419290884e63d8a5a17a Mon Sep 17 00:00:00 2001 From: Antoine Angenieux Date: Wed, 10 Oct 2012 17:06:52 +0200 Subject: [PATCH 03/15] [eclipse] define encoding for all files --- .settings/org.eclipse.core.resources.prefs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs index 586138d..839d647 100644 --- a/.settings/org.eclipse.core.resources.prefs +++ b/.settings/org.eclipse.core.resources.prefs @@ -1,3 +1,5 @@ -#Thu Feb 19 09:18:57 CET 2009 eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 encoding/=UTF-8 From 1c58c6fde3301bed2240128b03b5bbff3a33c6cc Mon Sep 17 00:00:00 2001 From: Antoine Angenieux Date: Wed, 10 Oct 2012 17:07:09 +0200 Subject: [PATCH 04/15] [new bindings] Create a few missing bindings --- .../options/axis/WHighChartAxisOptions.java | 13 ++ .../AbstractWHighChartPlotChartOptions.java | 15 +++ .../WHighChartPlotBarLabelsOptions.java | 84 ++++++++++++ .../plotoptions/WHighChartPlotBarOptions.java | 121 ++++++++++++++++++ .../WHighChartPlotColumnLabelsOptions.java | 96 ++++++++++++++ .../WHighChartPlotColumnOptions.java | 29 +++++ .../plotoptions/WHighChartPlotOptions.java | 8 +- .../plotoptions/WHighChartPlotPieOptions.java | 28 ++-- .../tooltip/WHighChartTooltipOptions.java | 13 +- 9 files changed, 387 insertions(+), 20 deletions(-) create mode 100644 src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotBarLabelsOptions.java create mode 100644 src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotBarOptions.java create mode 100644 src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnLabelsOptions.java diff --git a/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisOptions.java b/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisOptions.java index d8e9575..77d14fc 100644 --- a/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisOptions.java @@ -43,6 +43,8 @@ public enum AxisType private String gridLineColor; + private String alternateGridColor; + /** * An array of configuration objects for plot bands colouring parts of the plot area * background. Defaults to null. @@ -239,4 +241,15 @@ public WHighChartAxisOptions setGridLineColor(String gridLineColor) return this; } + public String getAlternateGridColor() + { + return alternateGridColor; + } + + public WHighChartAxisOptions setAlternateGridColor(String alternateGridColor) + { + this.alternateGridColor = alternateGridColor; + return this; + } + } diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/AbstractWHighChartPlotChartOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/AbstractWHighChartPlotChartOptions.java index 2ff6492..5812189 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/AbstractWHighChartPlotChartOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/AbstractWHighChartPlotChartOptions.java @@ -54,6 +54,8 @@ public class AbstractWHighChartPlotChartOptions +{ + private static final long serialVersionUID = 1L; + + /** + * Allow this series' points to be selected by clicking on the markers, bars or pie + * slices. Defaults to false. + */ + // private Boolean allowPointSelect; + + /** + * Enable or disable the initial animation when a series is displayed. Since version + * 2.1, the animation can be set as a configuration object. Please note that this + * option only applies to the initial animation of the series itself. For other + * animations, see #chart => animation and the animation parameter under the API + * methods. The following properties are supported: duration The duration of the + * animation in milliseconds. easing When using jQuery as the general framework, the + * easing can be set to linear or swing. More easing functions are available with the + * use of jQuery plug-ins, most notably the jQuery UI suite. See the jQuery docs. When + * using MooToos as the general framework, use the property name transition instead of + * easing. Defaults to true. + */ + private Boolean animation; + + /** + * The main color or the series. In line type series it applies to the line and the + * point markers unless otherwise specified. In bar type series it applies to the bars + * unless a color is specified per point. The default value is pulled from the + * options.colors array. + */ + private String color; + + /** + * You can set the cursor to "pointer" if you have click events attached to the + * series, to signal to the user that the points and lines can be clicked. Defaults to + * ''. + */ + // private WHighChartPointerType cursor; + + /** + * Defines the appearance of the data labels, static labels for each point. + */ + private WHighChartPlotBarLabelsOptions dataLabels; + + /** + * Whether to display this particular series or series type in the legend. Since 2.1, + * pies are not shown in the legend by default. Defaults to false. + */ + private Boolean showInLegend; + + private Number minPointLength; + + public Boolean getAnimation() + { + return animation; + } + + public WHighChartPlotBarOptions setAnimation(Boolean animation) + { + this.animation = animation; + return this; + } + + public String getColor() + { + return color; + } + + public WHighChartPlotBarOptions setColor(String color) + { + this.color = color; + return this; + } + + public WHighChartPlotBarLabelsOptions getDataLabels() + { + if (dataLabels == null) + dataLabels = new WHighChartPlotBarLabelsOptions(); + + return dataLabels; + } + + public WHighChartPlotBarOptions setDataLabels(WHighChartPlotBarLabelsOptions dataLabels) + { + this.dataLabels = dataLabels; + return this; + } + + public Boolean getShowInLegend() + { + return showInLegend; + } + + public WHighChartPlotBarOptions setShowInLegend(Boolean showInLegend) + { + this.showInLegend = showInLegend; + return this; + } + + public Number getMinPointLength() + { + return minPointLength; + } + + public WHighChartPlotBarOptions setMinPointLength(Number minPointLength) + { + this.minPointLength = minPointLength; + return this; + } + +} diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnLabelsOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnLabelsOptions.java new file mode 100644 index 0000000..614b0cf --- /dev/null +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnLabelsOptions.java @@ -0,0 +1,96 @@ +package nl.topicus.whighcharts.options.plotoptions; + +import java.io.Serializable; + +import nl.topicus.whighcharts.options.WHighChartFunction; +import nl.topicus.whighcharts.options.WHighChartFunctionString; + +import org.codehaus.jackson.annotate.JsonAutoDetect; +import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; +import org.codehaus.jackson.map.annotate.JsonSerialize; +import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; + +@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) +@JsonSerialize(include = Inclusion.NON_NULL) +public class WHighChartPlotColumnLabelsOptions implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** + * Enable or disable the data labels. Defaults to true. + */ + private Boolean enabled; + + private WHighChartFunction formatter; + + private String color; + + private Number x; + + private Number y; + + public Number getX() + { + return x; + } + + public WHighChartPlotColumnLabelsOptions setX(Number x) + { + this.x = x; + return this; + } + + public Number getY() + { + return y; + } + + public WHighChartPlotColumnLabelsOptions setY(Number y) + { + this.y = y; + return this; + } + + public Boolean getEnabled() + { + return enabled; + } + + public WHighChartPlotColumnLabelsOptions setEnabled(Boolean enabled) + { + this.enabled = enabled; + return this; + } + + public WHighChartFunction getFormatter() + { + return formatter; + } + + public WHighChartPlotColumnLabelsOptions setFormatter(WHighChartFunction formatter) + { + this.formatter = formatter; + return this; + } + + public WHighChartPlotColumnLabelsOptions setFormatter(String formatter) + { + return setFormatter(new WHighChartFunctionString(formatter)); + } + + public WHighChartPlotColumnLabelsOptions setSelection(String formatter) + { + return setFormatter(new WHighChartFunctionString(formatter)); + } + + public String getColor() + { + return color; + } + + public WHighChartPlotColumnLabelsOptions setColor(String color) + { + this.color = color; + return this; + } +} diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnOptions.java index 4e18b94..69fdbd4 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnOptions.java @@ -12,4 +12,33 @@ public class WHighChartPlotColumnOptions extends { private static final long serialVersionUID = 1L; + private WHighChartPlotColumnLabelsOptions dataLabels; + + // private WHighChartPlotStackingType stacking; + // + // public WHighChartPlotColumnOptions setStacking(WHighChartPlotStackingType stacking) + // { + // this.stacking = stacking; + // return this; + // } + // + // public WHighChartPlotStackingType getStacking() + // { + // return stacking; + // } + + public WHighChartPlotColumnLabelsOptions getDataLabels() + { + if (dataLabels == null) + dataLabels = new WHighChartPlotColumnLabelsOptions(); + + return dataLabels; + } + + public WHighChartPlotColumnOptions setDataLabels(WHighChartPlotColumnLabelsOptions dataLabels) + { + this.dataLabels = dataLabels; + return this; + } + } diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotOptions.java index ed206cc..b417228 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotOptions.java @@ -18,7 +18,7 @@ public class WHighChartPlotOptions implements Serializable private AbstractWHighChartPlotChartOptions areaspline; - private AbstractWHighChartPlotChartOptions bar; + private WHighChartPlotBarOptions bar; private WHighChartPlotColumnOptions column; @@ -60,15 +60,15 @@ public WHighChartPlotOptions setAreaspline(AbstractWHighChartPlotChartOptions ar return this; } - public AbstractWHighChartPlotChartOptions getBar() + public WHighChartPlotBarOptions getBar() { if (bar == null) - bar = new AbstractWHighChartPlotChartOptions(); + bar = new WHighChartPlotBarOptions(); return bar; } - public WHighChartPlotOptions setBar(AbstractWHighChartPlotChartOptions bar) + public WHighChartPlotOptions setBar(WHighChartPlotBarOptions bar) { this.bar = bar; return this; diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotPieOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotPieOptions.java index 23ff043..372b934 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotPieOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotPieOptions.java @@ -1,7 +1,5 @@ package nl.topicus.whighcharts.options.plotoptions; -import nl.topicus.whighcharts.options.WHighChartPointerType; - import org.codehaus.jackson.annotate.JsonAutoDetect; import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; import org.codehaus.jackson.map.annotate.JsonSerialize; @@ -14,12 +12,6 @@ public class WHighChartPlotPieOptions extends { private static final long serialVersionUID = 1L; - /** - * Allow this series' points to be selected by clicking on the markers, bars or pie - * slices. Defaults to false. - */ - private Boolean allowPointSelect; - /** * Enable or disable the initial animation when a series is displayed. Since version * 2.1, the animation can be set as a configuration object. Please note that this @@ -42,13 +34,6 @@ public class WHighChartPlotPieOptions extends */ private String color; - /** - * You can set the cursor to "pointer" if you have click events attached to the - * series, to signal to the user that the points and lines can be clicked. Defaults to - * ''. - */ - private WHighChartPointerType cursor; - /** * Defines the appearance of the data labels, static labels for each point. */ @@ -60,6 +45,8 @@ public class WHighChartPlotPieOptions extends */ private Boolean showInLegend; + private String size; + public Boolean getAnimation() { return animation; @@ -107,4 +94,15 @@ public WHighChartPlotPieOptions setShowInLegend(Boolean showInLegend) return this; } + public String getSize() + { + return size; + } + + public WHighChartPlotPieOptions setSize(String size) + { + this.size = size; + return this; + } + } diff --git a/src/main/java/nl/topicus/whighcharts/options/tooltip/WHighChartTooltipOptions.java b/src/main/java/nl/topicus/whighcharts/options/tooltip/WHighChartTooltipOptions.java index 3ab6a92..19ef068 100644 --- a/src/main/java/nl/topicus/whighcharts/options/tooltip/WHighChartTooltipOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/tooltip/WHighChartTooltipOptions.java @@ -25,7 +25,7 @@ public class WHighChartTooltipOptions implements Serializable private Number borderWidth; - // crosshairs: null, + private Boolean crosshairs; private Boolean enabled; @@ -84,6 +84,17 @@ public WHighChartTooltipOptions setBorderWidth(Number borderWidth) return this; } + public Boolean getCrosshairs() + { + return crosshairs; + } + + public WHighChartTooltipOptions setCrosshairs(Boolean crosshairs) + { + this.crosshairs = crosshairs; + return this; + } + public Boolean getEnabled() { return enabled; From aee096743292b80e1d276aa2a667626a2ce7dc6f Mon Sep 17 00:00:00 2001 From: Antoine Angenieux Date: Wed, 17 Apr 2013 12:45:39 +0200 Subject: [PATCH 05/15] [Stacking options bindings] Added bindings Added bindings for stacking options --- .../options/WHighChartStackingType.java | 7 +++++++ .../AbstractWHighChartPlotChartOptions.java | 15 +++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/main/java/nl/topicus/whighcharts/options/WHighChartStackingType.java diff --git a/src/main/java/nl/topicus/whighcharts/options/WHighChartStackingType.java b/src/main/java/nl/topicus/whighcharts/options/WHighChartStackingType.java new file mode 100644 index 0000000..14b0655 --- /dev/null +++ b/src/main/java/nl/topicus/whighcharts/options/WHighChartStackingType.java @@ -0,0 +1,7 @@ +package nl.topicus.whighcharts.options; + +public enum WHighChartStackingType +{ + normal, + percent; +} diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/AbstractWHighChartPlotChartOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/AbstractWHighChartPlotChartOptions.java index 5812189..8efb0c7 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/AbstractWHighChartPlotChartOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/AbstractWHighChartPlotChartOptions.java @@ -3,6 +3,7 @@ import java.io.Serializable; import nl.topicus.whighcharts.options.WHighChartPointerType; +import nl.topicus.whighcharts.options.WHighChartStackingType; import nl.topicus.whighcharts.options.chart.WHighChartChartEventsOptions; import org.codehaus.jackson.annotate.JsonAutoDetect; @@ -56,6 +57,8 @@ public class AbstractWHighChartPlotChartOptions Date: Wed, 25 Feb 2015 11:40:37 +0100 Subject: [PATCH 06/15] [Configuration] Specify version for slf4j --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 04e7704..0381b4d 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ UTF-8 6.0-SNAPSHOT 6.0-SNAPSHOT - [1.6,1.6.10] + 1.6.1 4.10 6.1.26 From 0dc862697573ac6e8ae80c3224df02645b131739 Mon Sep 17 00:00:00 2001 From: Antoine Angenieux Date: Tue, 3 Nov 2015 14:13:17 +0100 Subject: [PATCH 07/15] [GIT] Add rebel.xml to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 52978a7..e9cd0a2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ target .settings/org.eclipse.m2e.core.prefs .classpath .project +rebel.xml From c9276493aab98770fa3ac33d84bc01ef83e52169 Mon Sep 17 00:00:00 2001 From: Antoine Angenieux Date: Wed, 30 Sep 2015 16:28:16 +0200 Subject: [PATCH 08/15] [Versioning] Impact upgrade of wicket version Impact upgrade from wicket-6.8.0 to wicket-6.20.0. Changes in wiquery and wicket stuff brings new version of Jackson (2.3.1) that required some migration. --- pom.xml | 7 ++++--- .../whighcharts/components/WHighChart.java | 17 +++++++++-------- .../options/WHighChartFunctionCallback.java | 3 ++- .../options/WHighChartFunctionString.java | 2 +- .../options/WHighChartGlobalSettings.java | 8 ++++---- .../options/WHighChartMarkerStateOptions.java | 8 ++++---- .../options/WHighChartMarkerStatesOptions.java | 8 ++++---- .../whighcharts/options/WHighChartOptions.java | 8 ++++---- .../options/WHighChartStyleOptions.java | 10 +++++----- .../WHighChartAxisDateTimeLabelFormats.java | 8 ++++---- .../axis/WHighChartAxisLabelsOptions.java | 8 ++++---- .../options/axis/WHighChartAxisOptions.java | 8 ++++---- .../axis/WHighChartAxisPlotBandsOptions.java | 8 ++++---- .../WHighChartAxisPlotLinesLabelOptions.java | 8 ++++---- .../axis/WHighChartAxisPlotLinesOptions.java | 8 ++++---- .../chart/WHighChartChartEventsOptions.java | 8 ++++---- .../options/chart/WHighChartChartOptions.java | 9 +++++---- .../credits/WHighChartCreditsOptions.java | 8 ++++---- .../WHighChartCreditsOptionsOptions.java | 8 ++++---- .../exporting/WHighChartExportingOptions.java | 8 ++++---- .../options/global/WHighChartGlobalOptions.java | 8 ++++---- .../jackson/ComponentMarkupIdSerializer.java | 13 +++++++------ .../jackson/ToStringNoQuoteSerializer.java | 12 ++++++------ ...tringNoQuoteWithCurlyBracketsSerializer.java | 12 ++++++------ .../options/labels/WHighChartLabelsOptions.java | 8 ++++---- .../options/lang/WHighChartLangOptions.java | 8 ++++---- .../options/legend/WHighChartLegendOptions.java | 8 ++++---- .../loading/WHighChartLoadingOptions.java | 8 ++++---- .../navigation/WHighChartNavigationOptions.java | 8 ++++---- .../AbstractWHighChartPlotChartOptions.java | 8 ++++---- .../WHighChartPlotAreaMarkerOptions.java | 8 ++++---- .../plotoptions/WHighChartPlotAreaOptions.java | 8 ++++---- .../WHighChartPlotBarLabelsOptions.java | 8 ++++---- .../plotoptions/WHighChartPlotBarOptions.java | 8 ++++---- .../WHighChartPlotChartPointEventsOptions.java | 8 ++++---- .../WHighChartPlotChartPointOptions.java | 8 ++++---- .../WHighChartPlotColumnLabelsOptions.java | 8 ++++---- .../WHighChartPlotColumnOptions.java | 8 ++++---- .../plotoptions/WHighChartPlotOptions.java | 8 ++++---- .../WHighChartPlotPieLabelsOptions.java | 8 ++++---- .../plotoptions/WHighChartPlotPieOptions.java | 8 ++++---- .../WHighChartPlotSeriesOptions.java | 8 ++++---- .../point/WHighChartPointEventsOptions.java | 8 ++++---- .../options/point/WHighChartPointOptions.java | 8 ++++---- .../options/series/AbstractSeries.java | 8 ++++---- .../options/series/KeyValueSeries.java | 8 ++++---- .../options/series/KeyValueSeriesEntry.java | 2 +- .../options/series/ObjectSeries.java | 8 ++++---- .../options/series/ObjectSeriesEntry.java | 8 ++++---- .../whighcharts/options/series/ValueSeries.java | 8 ++++---- .../options/series/ValueSeriesEntry.java | 2 +- .../WHighChartSeriesDataLabelsOptions.java | 8 ++++---- .../options/title/WHighChartTitleOptions.java | 8 ++++---- .../tooltip/WHighChartTooltipOptions.java | 8 ++++---- 54 files changed, 219 insertions(+), 214 deletions(-) diff --git a/pom.xml b/pom.xml index 0381b4d..7560fce 100644 --- a/pom.xml +++ b/pom.xml @@ -20,8 +20,9 @@ UTF-8 - 6.0-SNAPSHOT - 6.0-SNAPSHOT + 6.20.0 + 6.13.0 + 6.9.2 1.6.1 4.10 6.1.26 @@ -93,7 +94,7 @@ org.odlabs.wiquery wiquery-compressor - ${wiquery.version} + ${wiquery-compressor.version} diff --git a/src/main/java/nl/topicus/whighcharts/components/WHighChart.java b/src/main/java/nl/topicus/whighcharts/components/WHighChart.java index 7ed3671..59d15de 100644 --- a/src/main/java/nl/topicus/whighcharts/components/WHighChart.java +++ b/src/main/java/nl/topicus/whighcharts/components/WHighChart.java @@ -17,14 +17,15 @@ import org.apache.wicket.markup.head.OnDomReadyHeaderItem; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.model.IModel; -import org.codehaus.jackson.JsonGenerationException; -import org.codehaus.jackson.JsonGenerator; -import org.codehaus.jackson.map.JsonMappingException; -import org.codehaus.jackson.map.ObjectMapper; -import org.codehaus.jackson.map.SerializationConfig; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; import org.odlabs.wiquery.core.javascript.JsStatement; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + public class WHighChart> extends WebMarkupContainer { private static final long serialVersionUID = 1L; @@ -74,13 +75,13 @@ && getOptions().getExporting().getEnabled().booleanValue()) public JsStatement statement() { ObjectMapper mapper = new ObjectMapper(); - mapper.getSerializationConfig().withSerializationInclusion(Inclusion.NON_NULL); + mapper.getSerializationConfig().withSerializationInclusion(Include.NON_NULL); mapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false); if (Application.exists() && RuntimeConfigurationType.DEVELOPMENT .equals(Application.get().getConfigurationType())) - mapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true); + mapper.configure(SerializationFeature.INDENT_OUTPUT, true); String optionsStr = "{}"; String globalOptions = "{}"; diff --git a/src/main/java/nl/topicus/whighcharts/options/WHighChartFunctionCallback.java b/src/main/java/nl/topicus/whighcharts/options/WHighChartFunctionCallback.java index 028e8bf..a49409a 100644 --- a/src/main/java/nl/topicus/whighcharts/options/WHighChartFunctionCallback.java +++ b/src/main/java/nl/topicus/whighcharts/options/WHighChartFunctionCallback.java @@ -8,7 +8,8 @@ import org.apache.wicket.ajax.attributes.AjaxRequestAttributes; import org.apache.wicket.request.IRequestParameters; import org.apache.wicket.request.cycle.RequestCycle; -import org.codehaus.jackson.map.annotate.JsonSerialize; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; @JsonSerialize(using = ToStringNoQuoteSerializer.class) public class WHighChartFunctionCallback extends AbstractDefaultAjaxBehavior implements diff --git a/src/main/java/nl/topicus/whighcharts/options/WHighChartFunctionString.java b/src/main/java/nl/topicus/whighcharts/options/WHighChartFunctionString.java index a857709..b247d59 100644 --- a/src/main/java/nl/topicus/whighcharts/options/WHighChartFunctionString.java +++ b/src/main/java/nl/topicus/whighcharts/options/WHighChartFunctionString.java @@ -2,7 +2,7 @@ import nl.topicus.whighcharts.options.jackson.ToStringNoQuoteSerializer; -import org.codehaus.jackson.map.annotate.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; @JsonSerialize(using = ToStringNoQuoteSerializer.class) public class WHighChartFunctionString implements WHighChartFunction diff --git a/src/main/java/nl/topicus/whighcharts/options/WHighChartGlobalSettings.java b/src/main/java/nl/topicus/whighcharts/options/WHighChartGlobalSettings.java index a53581c..b6779f3 100644 --- a/src/main/java/nl/topicus/whighcharts/options/WHighChartGlobalSettings.java +++ b/src/main/java/nl/topicus/whighcharts/options/WHighChartGlobalSettings.java @@ -5,10 +5,10 @@ import nl.topicus.whighcharts.options.global.WHighChartGlobalOptions; import nl.topicus.whighcharts.options.lang.WHighChartLangOptions; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; /** * Contains settings that must be set globally using Highcharts.setOptions, rather than diff --git a/src/main/java/nl/topicus/whighcharts/options/WHighChartMarkerStateOptions.java b/src/main/java/nl/topicus/whighcharts/options/WHighChartMarkerStateOptions.java index 247752e..fe91ea3 100644 --- a/src/main/java/nl/topicus/whighcharts/options/WHighChartMarkerStateOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/WHighChartMarkerStateOptions.java @@ -2,10 +2,10 @@ import java.io.Serializable; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/WHighChartMarkerStatesOptions.java b/src/main/java/nl/topicus/whighcharts/options/WHighChartMarkerStatesOptions.java index f5f9968..0f84f9f 100644 --- a/src/main/java/nl/topicus/whighcharts/options/WHighChartMarkerStatesOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/WHighChartMarkerStatesOptions.java @@ -2,10 +2,10 @@ import java.io.Serializable; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/WHighChartOptions.java b/src/main/java/nl/topicus/whighcharts/options/WHighChartOptions.java index 849275b..a84006d 100644 --- a/src/main/java/nl/topicus/whighcharts/options/WHighChartOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/WHighChartOptions.java @@ -22,10 +22,10 @@ import nl.topicus.whighcharts.options.title.WHighChartTitleOptions; import nl.topicus.whighcharts.options.tooltip.WHighChartTooltipOptions; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/WHighChartStyleOptions.java b/src/main/java/nl/topicus/whighcharts/options/WHighChartStyleOptions.java index aedc77e..cd09b66 100644 --- a/src/main/java/nl/topicus/whighcharts/options/WHighChartStyleOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/WHighChartStyleOptions.java @@ -2,10 +2,10 @@ import java.io.Serializable; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) @@ -14,7 +14,7 @@ public class WHighChartStyleOptions implements Serializable private static final long serialVersionUID = 1L; private String color; - + private String fontSize; public WHighChartStyleOptions setColor(String color) diff --git a/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisDateTimeLabelFormats.java b/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisDateTimeLabelFormats.java index 63141f0..dd52d97 100644 --- a/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisDateTimeLabelFormats.java +++ b/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisDateTimeLabelFormats.java @@ -2,10 +2,10 @@ import java.io.Serializable; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisLabelsOptions.java b/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisLabelsOptions.java index f45f313..d82fd8b 100644 --- a/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisLabelsOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisLabelsOptions.java @@ -7,10 +7,10 @@ import nl.topicus.whighcharts.options.WHighChartHorizontalAlignmentType; import nl.topicus.whighcharts.options.jackson.ToStringNoQuoteWithCurlyBracketsSerializer; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisOptions.java b/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisOptions.java index 77d14fc..c63bd2b 100644 --- a/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisOptions.java @@ -7,10 +7,10 @@ import nl.topicus.whighcharts.options.title.WHighChartTitleOptions; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisPlotBandsOptions.java b/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisPlotBandsOptions.java index 17bd38b..ef4b97c 100644 --- a/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisPlotBandsOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisPlotBandsOptions.java @@ -2,10 +2,10 @@ import java.io.Serializable; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisPlotLinesLabelOptions.java b/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisPlotLinesLabelOptions.java index e43f22a..0a168b1 100644 --- a/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisPlotLinesLabelOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisPlotLinesLabelOptions.java @@ -4,10 +4,10 @@ import nl.topicus.whighcharts.options.WHighChartStyleOptions; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisPlotLinesOptions.java b/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisPlotLinesOptions.java index a5219b0..32c7c4d 100644 --- a/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisPlotLinesOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/axis/WHighChartAxisPlotLinesOptions.java @@ -2,10 +2,10 @@ import java.io.Serializable; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/chart/WHighChartChartEventsOptions.java b/src/main/java/nl/topicus/whighcharts/options/chart/WHighChartChartEventsOptions.java index 93be912..deb6023 100644 --- a/src/main/java/nl/topicus/whighcharts/options/chart/WHighChartChartEventsOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/chart/WHighChartChartEventsOptions.java @@ -5,10 +5,10 @@ import nl.topicus.whighcharts.options.WHighChartFunction; import nl.topicus.whighcharts.options.WHighChartFunctionString; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/chart/WHighChartChartOptions.java b/src/main/java/nl/topicus/whighcharts/options/chart/WHighChartChartOptions.java index 4b66afb..bb42bb7 100644 --- a/src/main/java/nl/topicus/whighcharts/options/chart/WHighChartChartOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/chart/WHighChartChartOptions.java @@ -8,10 +8,11 @@ import nl.topicus.whighcharts.options.jackson.ToStringNoQuoteWithCurlyBracketsSerializer; import org.apache.wicket.Component; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; /** * Options regarding the chart area and plot area as well as general chart options. diff --git a/src/main/java/nl/topicus/whighcharts/options/credits/WHighChartCreditsOptions.java b/src/main/java/nl/topicus/whighcharts/options/credits/WHighChartCreditsOptions.java index 0d161ad..8247fa8 100644 --- a/src/main/java/nl/topicus/whighcharts/options/credits/WHighChartCreditsOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/credits/WHighChartCreditsOptions.java @@ -4,10 +4,10 @@ import nl.topicus.whighcharts.options.jackson.ToStringNoQuoteWithCurlyBracketsSerializer; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; /** * Highchart by default puts a credits label in the lower right corner of the chart. This diff --git a/src/main/java/nl/topicus/whighcharts/options/credits/WHighChartCreditsOptionsOptions.java b/src/main/java/nl/topicus/whighcharts/options/credits/WHighChartCreditsOptionsOptions.java index 1b5f73d..d09f96e 100644 --- a/src/main/java/nl/topicus/whighcharts/options/credits/WHighChartCreditsOptionsOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/credits/WHighChartCreditsOptionsOptions.java @@ -5,10 +5,10 @@ import nl.topicus.whighcharts.options.WHighChartHorizontalAlignmentType; import nl.topicus.whighcharts.options.WHighChartVerticalAlignmentType; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/exporting/WHighChartExportingOptions.java b/src/main/java/nl/topicus/whighcharts/options/exporting/WHighChartExportingOptions.java index a3c75e7..7a5cd62 100644 --- a/src/main/java/nl/topicus/whighcharts/options/exporting/WHighChartExportingOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/exporting/WHighChartExportingOptions.java @@ -2,10 +2,10 @@ import java.io.Serializable; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/global/WHighChartGlobalOptions.java b/src/main/java/nl/topicus/whighcharts/options/global/WHighChartGlobalOptions.java index a99ed49..64b8e72 100644 --- a/src/main/java/nl/topicus/whighcharts/options/global/WHighChartGlobalOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/global/WHighChartGlobalOptions.java @@ -2,10 +2,10 @@ import java.io.Serializable; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; /** * Global options that don't apply to each chart. These options, like the lang options, diff --git a/src/main/java/nl/topicus/whighcharts/options/jackson/ComponentMarkupIdSerializer.java b/src/main/java/nl/topicus/whighcharts/options/jackson/ComponentMarkupIdSerializer.java index 5607f01..dc9a487 100644 --- a/src/main/java/nl/topicus/whighcharts/options/jackson/ComponentMarkupIdSerializer.java +++ b/src/main/java/nl/topicus/whighcharts/options/jackson/ComponentMarkupIdSerializer.java @@ -4,13 +4,14 @@ import java.lang.reflect.Type; import org.apache.wicket.Component; -import org.codehaus.jackson.JsonGenerationException; -import org.codehaus.jackson.JsonGenerator; -import org.codehaus.jackson.JsonNode; -import org.codehaus.jackson.map.SerializerProvider; -import org.codehaus.jackson.map.ser.std.SerializerBase; -public class ComponentMarkupIdSerializer extends SerializerBase +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +public class ComponentMarkupIdSerializer extends StdSerializer { public ComponentMarkupIdSerializer() { diff --git a/src/main/java/nl/topicus/whighcharts/options/jackson/ToStringNoQuoteSerializer.java b/src/main/java/nl/topicus/whighcharts/options/jackson/ToStringNoQuoteSerializer.java index b328de3..3fbb101 100644 --- a/src/main/java/nl/topicus/whighcharts/options/jackson/ToStringNoQuoteSerializer.java +++ b/src/main/java/nl/topicus/whighcharts/options/jackson/ToStringNoQuoteSerializer.java @@ -3,13 +3,13 @@ import java.io.IOException; import java.lang.reflect.Type; -import org.codehaus.jackson.JsonGenerationException; -import org.codehaus.jackson.JsonGenerator; -import org.codehaus.jackson.JsonNode; -import org.codehaus.jackson.map.SerializerProvider; -import org.codehaus.jackson.map.ser.std.SerializerBase; +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; -public class ToStringNoQuoteSerializer extends SerializerBase +public class ToStringNoQuoteSerializer extends StdSerializer { public ToStringNoQuoteSerializer() { diff --git a/src/main/java/nl/topicus/whighcharts/options/jackson/ToStringNoQuoteWithCurlyBracketsSerializer.java b/src/main/java/nl/topicus/whighcharts/options/jackson/ToStringNoQuoteWithCurlyBracketsSerializer.java index c8cf460..904c30d 100644 --- a/src/main/java/nl/topicus/whighcharts/options/jackson/ToStringNoQuoteWithCurlyBracketsSerializer.java +++ b/src/main/java/nl/topicus/whighcharts/options/jackson/ToStringNoQuoteWithCurlyBracketsSerializer.java @@ -3,13 +3,13 @@ import java.io.IOException; import java.lang.reflect.Type; -import org.codehaus.jackson.JsonGenerationException; -import org.codehaus.jackson.JsonGenerator; -import org.codehaus.jackson.JsonNode; -import org.codehaus.jackson.map.SerializerProvider; -import org.codehaus.jackson.map.ser.std.SerializerBase; +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; -public class ToStringNoQuoteWithCurlyBracketsSerializer extends SerializerBase +public class ToStringNoQuoteWithCurlyBracketsSerializer extends StdSerializer { public ToStringNoQuoteWithCurlyBracketsSerializer() { diff --git a/src/main/java/nl/topicus/whighcharts/options/labels/WHighChartLabelsOptions.java b/src/main/java/nl/topicus/whighcharts/options/labels/WHighChartLabelsOptions.java index 3e7e975..77a9133 100644 --- a/src/main/java/nl/topicus/whighcharts/options/labels/WHighChartLabelsOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/labels/WHighChartLabelsOptions.java @@ -5,10 +5,10 @@ import nl.topicus.whighcharts.options.jackson.ToStringNoQuoteWithCurlyBracketsSerializer; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/lang/WHighChartLangOptions.java b/src/main/java/nl/topicus/whighcharts/options/lang/WHighChartLangOptions.java index 5d663f1..c288418 100644 --- a/src/main/java/nl/topicus/whighcharts/options/lang/WHighChartLangOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/lang/WHighChartLangOptions.java @@ -4,10 +4,10 @@ import java.util.Arrays; import java.util.List; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; /** * Language object. The language object is global and it can't be set on each chart diff --git a/src/main/java/nl/topicus/whighcharts/options/legend/WHighChartLegendOptions.java b/src/main/java/nl/topicus/whighcharts/options/legend/WHighChartLegendOptions.java index 5a5eddc..dccc19f 100644 --- a/src/main/java/nl/topicus/whighcharts/options/legend/WHighChartLegendOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/legend/WHighChartLegendOptions.java @@ -6,10 +6,10 @@ import nl.topicus.whighcharts.options.WHighChartVerticalAlignmentType; import nl.topicus.whighcharts.options.jackson.ToStringNoQuoteWithCurlyBracketsSerializer; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/loading/WHighChartLoadingOptions.java b/src/main/java/nl/topicus/whighcharts/options/loading/WHighChartLoadingOptions.java index 9f3d5a9..325e2e2 100644 --- a/src/main/java/nl/topicus/whighcharts/options/loading/WHighChartLoadingOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/loading/WHighChartLoadingOptions.java @@ -4,10 +4,10 @@ import nl.topicus.whighcharts.options.jackson.ToStringNoQuoteWithCurlyBracketsSerializer; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/navigation/WHighChartNavigationOptions.java b/src/main/java/nl/topicus/whighcharts/options/navigation/WHighChartNavigationOptions.java index 6551b1b..99eb6eb 100644 --- a/src/main/java/nl/topicus/whighcharts/options/navigation/WHighChartNavigationOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/navigation/WHighChartNavigationOptions.java @@ -2,10 +2,10 @@ import java.io.Serializable; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/AbstractWHighChartPlotChartOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/AbstractWHighChartPlotChartOptions.java index 8efb0c7..d6bf52d 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/AbstractWHighChartPlotChartOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/AbstractWHighChartPlotChartOptions.java @@ -6,10 +6,10 @@ import nl.topicus.whighcharts.options.WHighChartStackingType; import nl.topicus.whighcharts.options.chart.WHighChartChartEventsOptions; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotAreaMarkerOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotAreaMarkerOptions.java index 306643f..d05f9d9 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotAreaMarkerOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotAreaMarkerOptions.java @@ -4,10 +4,10 @@ import nl.topicus.whighcharts.options.WHighChartMarkerStatesOptions; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotAreaOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotAreaOptions.java index 6765e2b..8a92172 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotAreaOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotAreaOptions.java @@ -1,9 +1,9 @@ package nl.topicus.whighcharts.options.plotoptions; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotBarLabelsOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotBarLabelsOptions.java index 7eaad42..4a68c84 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotBarLabelsOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotBarLabelsOptions.java @@ -5,10 +5,10 @@ import nl.topicus.whighcharts.options.WHighChartFunction; import nl.topicus.whighcharts.options.WHighChartFunctionString; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotBarOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotBarOptions.java index 275a47e..ee0e6fc 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotBarOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotBarOptions.java @@ -1,9 +1,9 @@ package nl.topicus.whighcharts.options.plotoptions; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotChartPointEventsOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotChartPointEventsOptions.java index 6954ccb..7092991 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotChartPointEventsOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotChartPointEventsOptions.java @@ -5,10 +5,10 @@ import nl.topicus.whighcharts.options.WHighChartFunction; import nl.topicus.whighcharts.options.WHighChartFunctionString; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotChartPointOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotChartPointOptions.java index 0fc6a58..3fd9c0a 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotChartPointOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotChartPointOptions.java @@ -2,10 +2,10 @@ import java.io.Serializable; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnLabelsOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnLabelsOptions.java index 614b0cf..9ed687d 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnLabelsOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnLabelsOptions.java @@ -5,10 +5,10 @@ import nl.topicus.whighcharts.options.WHighChartFunction; import nl.topicus.whighcharts.options.WHighChartFunctionString; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnOptions.java index 69fdbd4..232253e 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotColumnOptions.java @@ -1,9 +1,9 @@ package nl.topicus.whighcharts.options.plotoptions; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotOptions.java index b417228..61c51f1 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotOptions.java @@ -2,10 +2,10 @@ import java.io.Serializable; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotPieLabelsOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotPieLabelsOptions.java index 4458776..3ec5e72 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotPieLabelsOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotPieLabelsOptions.java @@ -5,10 +5,10 @@ import nl.topicus.whighcharts.options.WHighChartFunction; import nl.topicus.whighcharts.options.WHighChartFunctionString; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotPieOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotPieOptions.java index 372b934..285b4ba 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotPieOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotPieOptions.java @@ -1,9 +1,9 @@ package nl.topicus.whighcharts.options.plotoptions; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotSeriesOptions.java b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotSeriesOptions.java index 1b43b0f..6f3f65e 100644 --- a/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotSeriesOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/plotoptions/WHighChartPlotSeriesOptions.java @@ -4,10 +4,10 @@ import nl.topicus.whighcharts.options.point.WHighChartPointOptions; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/point/WHighChartPointEventsOptions.java b/src/main/java/nl/topicus/whighcharts/options/point/WHighChartPointEventsOptions.java index 7a37957..2b7af79 100644 --- a/src/main/java/nl/topicus/whighcharts/options/point/WHighChartPointEventsOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/point/WHighChartPointEventsOptions.java @@ -5,10 +5,10 @@ import nl.topicus.whighcharts.options.WHighChartFunction; import nl.topicus.whighcharts.options.WHighChartFunctionString; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/point/WHighChartPointOptions.java b/src/main/java/nl/topicus/whighcharts/options/point/WHighChartPointOptions.java index e883fd8..984cc46 100644 --- a/src/main/java/nl/topicus/whighcharts/options/point/WHighChartPointOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/point/WHighChartPointOptions.java @@ -2,10 +2,10 @@ import java.io.Serializable; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/series/AbstractSeries.java b/src/main/java/nl/topicus/whighcharts/options/series/AbstractSeries.java index f77f1a7..9051664 100644 --- a/src/main/java/nl/topicus/whighcharts/options/series/AbstractSeries.java +++ b/src/main/java/nl/topicus/whighcharts/options/series/AbstractSeries.java @@ -5,10 +5,10 @@ import nl.topicus.whighcharts.options.chart.WHighChartChartOptionsType; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/series/KeyValueSeries.java b/src/main/java/nl/topicus/whighcharts/options/series/KeyValueSeries.java index fb80649..73150f3 100644 --- a/src/main/java/nl/topicus/whighcharts/options/series/KeyValueSeries.java +++ b/src/main/java/nl/topicus/whighcharts/options/series/KeyValueSeries.java @@ -1,9 +1,9 @@ package nl.topicus.whighcharts.options.series; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/series/KeyValueSeriesEntry.java b/src/main/java/nl/topicus/whighcharts/options/series/KeyValueSeriesEntry.java index 55e5e45..d32a294 100644 --- a/src/main/java/nl/topicus/whighcharts/options/series/KeyValueSeriesEntry.java +++ b/src/main/java/nl/topicus/whighcharts/options/series/KeyValueSeriesEntry.java @@ -2,7 +2,7 @@ import java.util.Arrays; -import org.codehaus.jackson.annotate.JsonValue; +import com.fasterxml.jackson.annotation.JsonValue; public class KeyValueSeriesEntry implements ISeriesEntry { diff --git a/src/main/java/nl/topicus/whighcharts/options/series/ObjectSeries.java b/src/main/java/nl/topicus/whighcharts/options/series/ObjectSeries.java index 7fceac5..8f3860b 100644 --- a/src/main/java/nl/topicus/whighcharts/options/series/ObjectSeries.java +++ b/src/main/java/nl/topicus/whighcharts/options/series/ObjectSeries.java @@ -1,9 +1,9 @@ package nl.topicus.whighcharts.options.series; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/series/ObjectSeriesEntry.java b/src/main/java/nl/topicus/whighcharts/options/series/ObjectSeriesEntry.java index 8472ec1..80478b8 100644 --- a/src/main/java/nl/topicus/whighcharts/options/series/ObjectSeriesEntry.java +++ b/src/main/java/nl/topicus/whighcharts/options/series/ObjectSeriesEntry.java @@ -1,9 +1,9 @@ package nl.topicus.whighcharts.options.series; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/series/ValueSeries.java b/src/main/java/nl/topicus/whighcharts/options/series/ValueSeries.java index b32cedd..8d93d99 100644 --- a/src/main/java/nl/topicus/whighcharts/options/series/ValueSeries.java +++ b/src/main/java/nl/topicus/whighcharts/options/series/ValueSeries.java @@ -1,9 +1,9 @@ package nl.topicus.whighcharts.options.series; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/series/ValueSeriesEntry.java b/src/main/java/nl/topicus/whighcharts/options/series/ValueSeriesEntry.java index ffc6e0b..3ec0c5d 100644 --- a/src/main/java/nl/topicus/whighcharts/options/series/ValueSeriesEntry.java +++ b/src/main/java/nl/topicus/whighcharts/options/series/ValueSeriesEntry.java @@ -1,6 +1,6 @@ package nl.topicus.whighcharts.options.series; -import org.codehaus.jackson.annotate.JsonValue; +import com.fasterxml.jackson.annotation.JsonValue; public class ValueSeriesEntry implements ISeriesEntry { diff --git a/src/main/java/nl/topicus/whighcharts/options/series/WHighChartSeriesDataLabelsOptions.java b/src/main/java/nl/topicus/whighcharts/options/series/WHighChartSeriesDataLabelsOptions.java index 523a52d..fdbd34e 100644 --- a/src/main/java/nl/topicus/whighcharts/options/series/WHighChartSeriesDataLabelsOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/series/WHighChartSeriesDataLabelsOptions.java @@ -7,10 +7,10 @@ import nl.topicus.whighcharts.options.WHighChartHorizontalAlignmentType; import nl.topicus.whighcharts.options.jackson.ToStringNoQuoteWithCurlyBracketsSerializer; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/title/WHighChartTitleOptions.java b/src/main/java/nl/topicus/whighcharts/options/title/WHighChartTitleOptions.java index 8e7f562..0159ed4 100644 --- a/src/main/java/nl/topicus/whighcharts/options/title/WHighChartTitleOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/title/WHighChartTitleOptions.java @@ -6,10 +6,10 @@ import nl.topicus.whighcharts.options.WHighChartVerticalAlignmentType; import nl.topicus.whighcharts.options.jackson.ToStringNoQuoteWithCurlyBracketsSerializer; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) diff --git a/src/main/java/nl/topicus/whighcharts/options/tooltip/WHighChartTooltipOptions.java b/src/main/java/nl/topicus/whighcharts/options/tooltip/WHighChartTooltipOptions.java index 19ef068..e12f8f0 100644 --- a/src/main/java/nl/topicus/whighcharts/options/tooltip/WHighChartTooltipOptions.java +++ b/src/main/java/nl/topicus/whighcharts/options/tooltip/WHighChartTooltipOptions.java @@ -6,10 +6,10 @@ import nl.topicus.whighcharts.options.WHighChartFunctionString; import nl.topicus.whighcharts.options.jackson.ToStringNoQuoteWithCurlyBracketsSerializer; -import org.codehaus.jackson.annotate.JsonAutoDetect; -import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion; @JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) @JsonSerialize(include = Inclusion.NON_NULL) From 316e99652e60d773b658e52c221be4f803833d9b Mon Sep 17 00:00:00 2001 From: Antoine Angenieux Date: Fri, 2 Oct 2015 10:04:10 +0200 Subject: [PATCH 09/15] [Version] Change version to 6.0.13-aangenieux --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7560fce..34b5d71 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ nl.topicus whighcharts jar - 6.0-aangenieux-SNAPSHOT + 6.13.0-aangenieux-SNAPSHOT WHighCharts WiQuery-HighCharts bindings From 6aeaf1ef1f5520c885b0afa2204b70af80e9df2a Mon Sep 17 00:00:00 2001 From: Antoine Angenieux Date: Fri, 15 Jan 2016 15:57:31 +0100 Subject: [PATCH 10/15] [Versioning] Upgrade wicket to 6.21.0 --- pom.xml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 34b5d71..9aa2353 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 org.sonatype.oss @@ -7,9 +8,9 @@ 7 - - 2.2.1 - + + 2.2.1 + nl.topicus whighcharts @@ -20,7 +21,7 @@ UTF-8 - 6.20.0 + 6.21.0 6.13.0 6.9.2 1.6.1 From 1333647e08ede1bd8c5b62a97ee1f7e8d07cbe77 Mon Sep 17 00:00:00 2001 From: Antoine Angenieux Date: Sun, 6 Nov 2016 18:59:28 +0100 Subject: [PATCH 11/15] [Versioning] Upgrade to highcharts 4.2.5 --- pom.xml | 2 +- .../whighcharts/components/WHighChart.java | 44 +- ...tsDefaultsJavaScriptResourceReference.java | 22 + .../components/jquery.highcharts.defaults.js | 27 + .../components/jquery.highcharts.js | 34328 +++++++++------- .../components/jquery.highcharts.min.js | 239 - .../components/modules/exporting.src.js | 800 + .../modules/jquery.highcharts.exporting.js | 758 - .../jquery.highcharts.exporting.min.js | 24 - .../themes/jquery.highcharts.dark-blue.js | 263 - .../themes/jquery.highcharts.dark-green.js | 263 - .../themes/jquery.highcharts.gray.js | 262 - .../themes/jquery.highcharts.grid.js | 96 - 13 files changed, 20529 insertions(+), 16599 deletions(-) create mode 100644 src/main/java/nl/topicus/whighcharts/components/WHighChartsDefaultsJavaScriptResourceReference.java create mode 100644 src/main/resources/nl/topicus/whighcharts/components/jquery.highcharts.defaults.js delete mode 100644 src/main/resources/nl/topicus/whighcharts/components/jquery.highcharts.min.js create mode 100644 src/main/resources/nl/topicus/whighcharts/components/modules/exporting.src.js delete mode 100644 src/main/resources/nl/topicus/whighcharts/components/modules/jquery.highcharts.exporting.js delete mode 100644 src/main/resources/nl/topicus/whighcharts/components/modules/jquery.highcharts.exporting.min.js delete mode 100644 src/main/resources/nl/topicus/whighcharts/components/themes/jquery.highcharts.dark-blue.js delete mode 100644 src/main/resources/nl/topicus/whighcharts/components/themes/jquery.highcharts.dark-green.js delete mode 100644 src/main/resources/nl/topicus/whighcharts/components/themes/jquery.highcharts.gray.js delete mode 100644 src/main/resources/nl/topicus/whighcharts/components/themes/jquery.highcharts.grid.js diff --git a/pom.xml b/pom.xml index 9aa2353..fdff495 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ nl.topicus whighcharts jar - 6.13.0-aangenieux-SNAPSHOT + 6.13.0-aangenieux-highcharts-4.2.5-SNAPSHOT WHighCharts WiQuery-HighCharts bindings diff --git a/src/main/java/nl/topicus/whighcharts/components/WHighChart.java b/src/main/java/nl/topicus/whighcharts/components/WHighChart.java index 59d15de..1072cce 100644 --- a/src/main/java/nl/topicus/whighcharts/components/WHighChart.java +++ b/src/main/java/nl/topicus/whighcharts/components/WHighChart.java @@ -3,13 +3,6 @@ import java.io.IOException; import java.util.Collection; -import nl.topicus.whighcharts.components.modules.WHighChartsExportingJavaScriptResourceReference; -import nl.topicus.whighcharts.options.WHighChartGlobalSettings; -import nl.topicus.whighcharts.options.WHighChartOptions; -import nl.topicus.whighcharts.options.axis.IWHighChartAxisCategoriesProvider; -import nl.topicus.whighcharts.options.series.ISeries; -import nl.topicus.whighcharts.options.series.ISeriesEntry; - import org.apache.wicket.Application; import org.apache.wicket.RuntimeConfigurationType; import org.apache.wicket.markup.head.IHeaderResponse; @@ -26,6 +19,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import nl.topicus.whighcharts.components.modules.WHighChartsExportingJavaScriptResourceReference; +import nl.topicus.whighcharts.options.WHighChartGlobalSettings; +import nl.topicus.whighcharts.options.WHighChartOptions; +import nl.topicus.whighcharts.options.axis.IWHighChartAxisCategoriesProvider; +import nl.topicus.whighcharts.options.series.ISeries; +import nl.topicus.whighcharts.options.series.ISeriesEntry; + public class WHighChart> extends WebMarkupContainer { private static final long serialVersionUID = 1L; @@ -58,17 +58,19 @@ public WHighChartGlobalSettings getGlobalSettings() @Override public void renderHead(IHeaderResponse response) { - response.render(JavaScriptHeaderItem.forReference(WHighChartsJavaScriptResourceReference - .get())); + response.render( + JavaScriptHeaderItem.forReference(WHighChartsJavaScriptResourceReference.get())); + response.render( + JavaScriptHeaderItem.forReference(WHighChartsExtraJavaScriptResourceReference.get())); response.render(JavaScriptHeaderItem - .forReference(WHighChartsExtraJavaScriptResourceReference.get())); + .forReference(WHighChartsDefaultsJavaScriptResourceReference.get())); if (getOptions().getExporting().getEnabled() != null && getOptions().getExporting().getEnabled().booleanValue()) response.render(JavaScriptHeaderItem .forReference(WHighChartsExportingJavaScriptResourceReference.get())); - response.render(JavaScriptHeaderItem.forScript("var " + getMarkupId() + ";", "highchart_" - + getMarkupId())); + response.render(JavaScriptHeaderItem.forScript("var " + getMarkupId() + ";", + "highchart_" + getMarkupId())); response.render(OnDomReadyHeaderItem.forScript(statement().render().toString())); } @@ -78,9 +80,8 @@ public JsStatement statement() mapper.getSerializationConfig().withSerializationInclusion(Include.NON_NULL); mapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false); - if (Application.exists() - && RuntimeConfigurationType.DEVELOPMENT - .equals(Application.get().getConfigurationType())) + if (Application.exists() && RuntimeConfigurationType.DEVELOPMENT + .equals(Application.get().getConfigurationType())) mapper.configure(SerializationFeature.INDENT_OUTPUT, true); String optionsStr = "{}"; @@ -101,15 +102,15 @@ public JsStatement statement() if (getOptions().getxAxis().getCategories() == null || getOptions().getxAxis().getCategories().isEmpty()) { - getOptions().getxAxis().setCategories( - categoriesProvider.getxAxisCategories()); + getOptions().getxAxis() + .setCategories(categoriesProvider.getxAxisCategories()); } if (getOptions().getyAxis().getCategories() == null || getOptions().getyAxis().getCategories().isEmpty()) { - getOptions().getyAxis().setCategories( - categoriesProvider.getyAxisCategories()); + getOptions().getyAxis() + .setCategories(categoriesProvider.getyAxisCategories()); } } } @@ -131,9 +132,8 @@ public JsStatement statement() e.printStackTrace(); } - JsStatement jsStatement = - new JsStatement().append(globalOptions + getMarkupId() + " = new Highcharts.Chart( " - + optionsStr + " );\n"); + JsStatement jsStatement = new JsStatement().append( + globalOptions + getMarkupId() + " = new Highcharts.Chart( " + optionsStr + " );\n"); return jsStatement; } diff --git a/src/main/java/nl/topicus/whighcharts/components/WHighChartsDefaultsJavaScriptResourceReference.java b/src/main/java/nl/topicus/whighcharts/components/WHighChartsDefaultsJavaScriptResourceReference.java new file mode 100644 index 0000000..cce9b7a --- /dev/null +++ b/src/main/java/nl/topicus/whighcharts/components/WHighChartsDefaultsJavaScriptResourceReference.java @@ -0,0 +1,22 @@ +package nl.topicus.whighcharts.components; + +import org.apache.wicket.request.resource.JavaScriptResourceReference; + +public class WHighChartsDefaultsJavaScriptResourceReference extends JavaScriptResourceReference +{ + private static final long serialVersionUID = -4771815414204892357L; + + private static WHighChartsDefaultsJavaScriptResourceReference INSTANCE = + new WHighChartsDefaultsJavaScriptResourceReference(); + + private WHighChartsDefaultsJavaScriptResourceReference() + { + super(WHighChartsDefaultsJavaScriptResourceReference.class, + "jquery.highcharts.defaults.js"); + } + + public static WHighChartsDefaultsJavaScriptResourceReference get() + { + return INSTANCE; + } +} diff --git a/src/main/resources/nl/topicus/whighcharts/components/jquery.highcharts.defaults.js b/src/main/resources/nl/topicus/whighcharts/components/jquery.highcharts.defaults.js new file mode 100644 index 0000000..ab5aeee --- /dev/null +++ b/src/main/resources/nl/topicus/whighcharts/components/jquery.highcharts.defaults.js @@ -0,0 +1,27 @@ +Highcharts.setOptions({ + chart: { + zoomType: 'x', + style: { + fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif', + fontWeight: 300, + fontSize: '12px' + } + }, + plotOptions: { + series: { + animation: false, + marker : { + enabled : false + } + } + }, + credits: { + enabled: false + }, + title: { + text: null + }, + scrollbar: { + enabled: false + } +}); \ No newline at end of file diff --git a/src/main/resources/nl/topicus/whighcharts/components/jquery.highcharts.js b/src/main/resources/nl/topicus/whighcharts/components/jquery.highcharts.js index f477baf..24bdfed 100644 --- a/src/main/resources/nl/topicus/whighcharts/components/jquery.highcharts.js +++ b/src/main/resources/nl/topicus/whighcharts/components/jquery.highcharts.js @@ -2,14680 +2,19666 @@ // @compilation_level SIMPLE_OPTIMIZATIONS /** - * @license Highcharts JS v2.2.5 (2012-06-08) + * @license Highcharts JS v4.2.5-modified (2016-06-01) * - * (c) 2009-2011 Torstein Hønsi + * (c) 2009-2016 Torstein Honsi * * License: www.highcharts.com/license */ -// JSLint options: -/*global Highcharts, document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $, console */ - -(function () { +(function (root, factory) { + if (typeof module === 'object' && module.exports) { + module.exports = root.document ? + factory(root) : + factory; + } else { + root.Highcharts = factory(root); + } +}(typeof window !== 'undefined' ? window : this, function (win) { // eslint-disable-line no-undef // encapsulated variables -var UNDEFINED, - doc = document, - win = window, - math = Math, - mathRound = math.round, - mathFloor = math.floor, - mathCeil = math.ceil, - mathMax = math.max, - mathMin = math.min, - mathAbs = math.abs, - mathCos = math.cos, - mathSin = math.sin, - mathPI = math.PI, - deg2rad = mathPI * 2 / 360, - - - // some variables - userAgent = navigator.userAgent, - isIE = /msie/i.test(userAgent) && !win.opera, - docMode8 = doc.documentMode === 8, - isWebKit = /AppleWebKit/.test(userAgent), - isFirefox = /Firefox/.test(userAgent), - SVG_NS = 'http://www.w3.org/2000/svg', - hasSVG = !!doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect, - hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4, // issue #38 - useCanVG = !hasSVG && !isIE && !!doc.createElement('canvas').getContext, - Renderer, - hasTouch = doc.documentElement.ontouchstart !== UNDEFINED, - symbolSizes = {}, - idCounter = 0, - garbageBin, - defaultOptions, - dateFormat, // function - globalAnimation, - pathAnim, - timeUnits, - noop = function () {}, - - // some constants for frequently used strings - DIV = 'div', - ABSOLUTE = 'absolute', - RELATIVE = 'relative', - HIDDEN = 'hidden', - PREFIX = 'highcharts-', - VISIBLE = 'visible', - PX = 'px', - NONE = 'none', - M = 'M', - L = 'L', - /* - * Empirical lowest possible opacities for TRACKER_FILL - * IE6: 0.002 - * IE7: 0.002 - * IE8: 0.002 - * IE9: 0.00000000001 (unlimited) - * FF: 0.00000000001 (unlimited) - * Chrome: 0.000001 - * Safari: 0.000001 - * Opera: 0.00000000001 (unlimited) - */ - TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.000001 : 0.002) + ')', // invisible but clickable - //TRACKER_FILL = 'rgba(192,192,192,0.5)', - NORMAL_STATE = '', - HOVER_STATE = 'hover', - SELECT_STATE = 'select', - MILLISECOND = 'millisecond', - SECOND = 'second', - MINUTE = 'minute', - HOUR = 'hour', - DAY = 'day', - WEEK = 'week', - MONTH = 'month', - YEAR = 'year', - - // constants for attributes - FILL = 'fill', - LINEAR_GRADIENT = 'linearGradient', - STOPS = 'stops', - STROKE = 'stroke', - STROKE_WIDTH = 'stroke-width', - - // time methods, changed based on whether or not UTC is used - makeTime, - getMinutes, - getHours, - getDay, - getDate, - getMonth, - getFullYear, - setMinutes, - setHours, - setDate, - setMonth, - setFullYear, - - - // lookup over the types and the associated classes - seriesTypes = {}; - -// The Highcharts namespace -win.Highcharts = {}; - -/** - * Extend an object with the members of another - * @param {Object} a The object to be extended - * @param {Object} b The object to add to the first one - */ -function extend(a, b) { - var n; - if (!a) { - a = {}; - } - for (n in b) { - a[n] = b[n]; - } - return a; -} - -/** - * Take an array and turn into a hash with even number arguments as keys and odd numbers as - * values. Allows creating constants for commonly used style properties, attributes etc. - * Avoid it in performance critical situations like looping - */ -function hash() { - var i = 0, - args = arguments, - length = args.length, - obj = {}; - for (; i < length; i++) { - obj[args[i++]] = args[i]; - } - return obj; -} - -/** - * Shortcut for parseInt - * @param {Object} s - * @param {Number} mag Magnitude - */ -function pInt(s, mag) { - return parseInt(s, mag || 10); -} - -/** - * Check for string - * @param {Object} s - */ -function isString(s) { - return typeof s === 'string'; -} - -/** - * Check for object - * @param {Object} obj - */ -function isObject(obj) { - return typeof obj === 'object'; -} - -/** - * Check for array - * @param {Object} obj - */ -function isArray(obj) { - return Object.prototype.toString.call(obj) === '[object Array]'; -} - -/** - * Check for number - * @param {Object} n - */ -function isNumber(n) { - return typeof n === 'number'; -} - -function log2lin(num) { - return math.log(num) / math.LN10; -} -function lin2log(num) { - return math.pow(10, num); -} - -/** - * Remove last occurence of an item from an array - * @param {Array} arr - * @param {Mixed} item - */ -function erase(arr, item) { - var i = arr.length; - while (i--) { - if (arr[i] === item) { - arr.splice(i, 1); - break; - } - } - //return arr; -} - -/** - * Returns true if the object is not null or undefined. Like MooTools' $.defined. - * @param {Object} obj - */ -function defined(obj) { - return obj !== UNDEFINED && obj !== null; -} - -/** - * Set or get an attribute or an object of attributes. Can't use jQuery attr because - * it attempts to set expando properties on the SVG element, which is not allowed. - * - * @param {Object} elem The DOM element to receive the attribute(s) - * @param {String|Object} prop The property or an abject of key-value pairs - * @param {String} value The value if a single property is set - */ -function attr(elem, prop, value) { - var key, - setAttribute = 'setAttribute', - ret; - - // if the prop is a string - if (isString(prop)) { - // set the value - if (defined(value)) { - - elem[setAttribute](prop, value); - - // get the value - } else if (elem && elem.getAttribute) { // elem not defined when printing pie demo... - ret = elem.getAttribute(prop); - } - - // else if prop is defined, it is a hash of key/value pairs - } else if (defined(prop) && isObject(prop)) { - for (key in prop) { - elem[setAttribute](key, prop[key]); - } - } - return ret; -} -/** - * Check if an element is an array, and if not, make it into an array. Like - * MooTools' $.splat. - */ -function splat(obj) { - return isArray(obj) ? obj : [obj]; -} - - -/** - * Return the first value that is defined. Like MooTools' $.pick. - */ -function pick() { - var args = arguments, - i, - arg, - length = args.length; - for (i = 0; i < length; i++) { - arg = args[i]; - if (typeof arg !== 'undefined' && arg !== null) { - return arg; - } - } -} - -/** - * Set CSS on a given element - * @param {Object} el - * @param {Object} styles Style object with camel case property names - */ -function css(el, styles) { - if (isIE) { - if (styles && styles.opacity !== UNDEFINED) { - styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')'; - } - } - extend(el.style, styles); -} - -/** - * Utility function to create element with attributes and styles - * @param {Object} tag - * @param {Object} attribs - * @param {Object} styles - * @param {Object} parent - * @param {Object} nopad - */ -function createElement(tag, attribs, styles, parent, nopad) { - var el = doc.createElement(tag); - if (attribs) { - extend(el, attribs); - } - if (nopad) { - css(el, {padding: 0, border: NONE, margin: 0}); - } - if (styles) { - css(el, styles); - } - if (parent) { - parent.appendChild(el); - } - return el; -} - -/** - * Extend a prototyped class by new members - * @param {Object} parent - * @param {Object} members - */ -function extendClass(parent, members) { - var object = function () {}; - object.prototype = new parent(); - extend(object.prototype, members); - return object; -} - -/** - * How many decimals are there in a number - */ -function getDecimals(number) { - - number = (number || 0).toString(); - - return number.indexOf('.') > -1 ? - number.split('.')[1].length : - 0; -} - -/** - * Format a number and return a string based on input settings - * @param {Number} number The input number to format - * @param {Number} decimals The amount of decimals - * @param {String} decPoint The decimal point, defaults to the one given in the lang options - * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options - */ -function numberFormat(number, decimals, decPoint, thousandsSep) { - var lang = defaultOptions.lang, - // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/ - n = number, - c = decimals === -1 ? - getDecimals(number) : - (isNaN(decimals = mathAbs(decimals)) ? 2 : decimals), - d = decPoint === undefined ? lang.decimalPoint : decPoint, - t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep, - s = n < 0 ? "-" : "", - i = String(pInt(n = mathAbs(+n || 0).toFixed(c))), - j = i.length > 3 ? i.length % 3 : 0; - - return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + - (c ? d + mathAbs(n - i).toFixed(c).slice(2) : ""); -} - -/** - * Pad a string to a given length by adding 0 to the beginning - * @param {Number} number - * @param {Number} length - */ -function pad(number, length) { - // Create an array of the remaining length +1 and join it with 0's - return new Array((length || 2) + 1 - String(number).length).join(0) + number; -} - -/** - * Based on http://www.php.net/manual/en/function.strftime.php - * @param {String} format - * @param {Number} timestamp - * @param {Boolean} capitalize - */ -dateFormat = function (format, timestamp, capitalize) { - if (!defined(timestamp) || isNaN(timestamp)) { - return 'Invalid date'; - } - format = pick(format, '%Y-%m-%d %H:%M:%S'); - - var date = new Date(timestamp), - key, // used in for constuct below - // get the basic time values - hours = date[getHours](), - day = date[getDay](), - dayOfMonth = date[getDate](), - month = date[getMonth](), - fullYear = date[getFullYear](), - lang = defaultOptions.lang, - langWeekdays = lang.weekdays, - /* // uncomment this and the 'W' format key below to enable week numbers - weekNumber = function () { - var clone = new Date(date.valueOf()), - day = clone[getDay]() == 0 ? 7 : clone[getDay](), - dayNumber; - clone.setDate(clone[getDate]() + 4 - day); - dayNumber = mathFloor((clone.getTime() - new Date(clone[getFullYear](), 0, 1, -6)) / 86400000); - return 1 + mathFloor(dayNumber / 7); - }, - */ - - // list all format keys - replacements = { - - // Day - 'a': langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon' - 'A': langWeekdays[day], // Long weekday, like 'Monday' - 'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31 - 'e': dayOfMonth, // Day of the month, 1 through 31 - - // Week (none implemented) - //'W': weekNumber(), - - // Month - 'b': lang.shortMonths[month], // Short month, like 'Jan' - 'B': lang.months[month], // Long month, like 'January' - 'm': pad(month + 1), // Two digit month number, 01 through 12 - - // Year - 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009 - 'Y': fullYear, // Four digits year, like 2009 - - // Time - 'H': pad(hours), // Two digits hours in 24h format, 00 through 23 - 'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11 - 'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12 - 'M': pad(date[getMinutes]()), // Two digits minutes, 00 through 59 - 'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM - 'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM - 'S': pad(date.getSeconds()), // Two digits seconds, 00 through 59 - 'L': pad(mathRound(timestamp % 1000), 3) // Milliseconds (naming from Ruby) - }; - - - // do the replaces - for (key in replacements) { - format = format.replace('%' + key, replacements[key]); - } - - // Optionally capitalize the string and return - return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format; -}; - -/** - * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 - * @param {Number} interval - * @param {Array} multiples - * @param {Number} magnitude - * @param {Object} options - */ -function normalizeTickInterval(interval, multiples, magnitude, options) { - var normalized, i; - - // round to a tenfold of 1, 2, 2.5 or 5 - magnitude = pick(magnitude, 1); - normalized = interval / magnitude; - - // multiples for a linear scale - if (!multiples) { - multiples = [1, 2, 2.5, 5, 10]; - - // the allowDecimals option - if (options && options.allowDecimals === false) { - if (magnitude === 1) { - multiples = [1, 2, 5, 10]; - } else if (magnitude <= 0.1) { - multiples = [1 / magnitude]; - } - } - } - - // normalize the interval to the nearest multiple - for (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; -} - -/** - * Get a normalized tick interval for dates. Returns a configuration object with - * unit range (interval), count and name. Used to prepare data for getTimeTicks. - * Previously this logic was part of getTimeTicks, but as getTimeTicks now runs - * of segments in stock charts, the normalizing logic was extracted in order to - * prevent it for running over again for each segment having the same interval. - * #662, #697. - */ -function normalizeTimeTickInterval(tickInterval, unitsOption) { - var units = unitsOption || [[ - MILLISECOND, // unit name - [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples - ], [ - SECOND, - [1, 2, 5, 10, 15, 30] - ], [ - MINUTE, - [1, 2, 5, 10, 15, 30] - ], [ - HOUR, - [1, 2, 3, 4, 6, 8, 12] - ], [ - DAY, - [1, 2] - ], [ - WEEK, - [1, 2] - ], [ - MONTH, - [1, 2, 3, 4, 6] - ], [ - YEAR, - null - ]], - unit = units[units.length - 1], // default unit is years - interval = timeUnits[unit[0]], - multiples = unit[1], - count, - i; - - // loop through the units to find the one that best fits the tickInterval - for (i = 0; i < units.length; i++) { - unit = units[i]; - interval = timeUnits[unit[0]]; - multiples = unit[1]; - - - if (units[i + 1]) { - // lessThan is in the middle between the highest multiple and the next unit. - var lessThan = (interval * multiples[multiples.length - 1] + - timeUnits[units[i + 1][0]]) / 2; - - // break and keep the current unit - if (tickInterval <= lessThan) { - break; - } - } - } - - // prevent 2.5 years intervals, though 25, 250 etc. are allowed - if (interval === timeUnits[YEAR] && tickInterval < 5 * interval) { - multiples = [1, 2, 5]; - } - - // prevent 2.5 years intervals, though 25, 250 etc. are allowed - if (interval === timeUnits[YEAR] && tickInterval < 5 * interval) { - multiples = [1, 2, 5]; - } - - // get the count - count = normalizeTickInterval(tickInterval / interval, multiples); - - return { - unitRange: interval, - count: count, - unitName: unit[0] - }; -} - -/** - * Set the tick positions to a time unit that makes sense, for example - * on the first of each month or on every Monday. Return an array - * with the time positions. Used in datetime axes as well as for grouping - * data on a datetime axis. - * - * @param {Object} normalizedInterval The interval in axis values (ms) and the count - * @param {Number} min The minimum in axis values - * @param {Number} max The maximum in axis values - * @param {Number} startOfWeek - */ -function getTimeTicks(normalizedInterval, min, max, startOfWeek) { - var tickPositions = [], - i, - higherRanks = {}, - useUTC = defaultOptions.global.useUTC, - minYear, // used in months and years as a basis for Date.UTC() - minDate = new Date(min), - interval = normalizedInterval.unitRange, - count = normalizedInterval.count; - - - - if (interval >= timeUnits[SECOND]) { // second - minDate.setMilliseconds(0); - minDate.setSeconds(interval >= timeUnits[MINUTE] ? 0 : - count * mathFloor(minDate.getSeconds() / count)); - } - - if (interval >= timeUnits[MINUTE]) { // minute - minDate[setMinutes](interval >= timeUnits[HOUR] ? 0 : - count * mathFloor(minDate[getMinutes]() / count)); - } - - if (interval >= timeUnits[HOUR]) { // hour - minDate[setHours](interval >= timeUnits[DAY] ? 0 : - count * mathFloor(minDate[getHours]() / count)); - } - - if (interval >= timeUnits[DAY]) { // day - minDate[setDate](interval >= timeUnits[MONTH] ? 1 : - count * mathFloor(minDate[getDate]() / count)); - } - - if (interval >= timeUnits[MONTH]) { // month - minDate[setMonth](interval >= timeUnits[YEAR] ? 0 : - count * mathFloor(minDate[getMonth]() / count)); - minYear = minDate[getFullYear](); - } - - if (interval >= timeUnits[YEAR]) { // year - minYear -= minYear % count; - minDate[setFullYear](minYear); - } - - // week is a special case that runs outside the hierarchy - if (interval === timeUnits[WEEK]) { - // get start of current week, independent of count - minDate[setDate](minDate[getDate]() - minDate[getDay]() + - pick(startOfWeek, 1)); - } - - - // get tick positions - i = 1; - minYear = minDate[getFullYear](); - var time = minDate.getTime(), - minMonth = minDate[getMonth](), - minDateDate = minDate[getDate](), - timezoneOffset = useUTC ? - 0 : - (24 * 3600 * 1000 + minDate.getTimezoneOffset() * 60 * 1000) % (24 * 3600 * 1000); // #950 - - // iterate and add tick positions at appropriate values - while (time < max) { - tickPositions.push(time); - - // if the interval is years, use Date.UTC to increase years - if (interval === timeUnits[YEAR]) { - time = makeTime(minYear + i * count, 0); - - // if the interval is months, use Date.UTC to increase months - } else if (interval === timeUnits[MONTH]) { - time = makeTime(minYear, minMonth + i * count); - - // if we're using global time, the interval is not fixed as it jumps - // one hour at the DST crossover - } else if (!useUTC && (interval === timeUnits[DAY] || interval === timeUnits[WEEK])) { - time = makeTime(minYear, minMonth, minDateDate + - i * count * (interval === timeUnits[DAY] ? 1 : 7)); - - // else, the interval is fixed and we use simple addition - } else { - time += interval * count; - - // mark new days if the time is dividable by day - if (interval <= timeUnits[HOUR] && time % timeUnits[DAY] === timezoneOffset) { - higherRanks[time] = DAY; - } - } - - i++; - } - - // push the last time - tickPositions.push(time); - - // record information on the chosen unit - for dynamic label formatter - tickPositions.info = extend(normalizedInterval, { - higherRanks: higherRanks, - totalRange: interval * count - }); - - return tickPositions; -} - -/** - * Helper class that contains variuos counters that are local to the chart. - */ -function ChartCounters() { - this.color = 0; - this.symbol = 0; -} - -ChartCounters.prototype = { - /** - * Wraps the color counter if it reaches the specified length. - */ - wrapColor: function (length) { - if (this.color >= length) { - this.color = 0; - } - }, - - /** - * Wraps the symbol counter if it reaches the specified length. - */ - wrapSymbol: function (length) { - if (this.symbol >= length) { - this.symbol = 0; - } - } -}; - - -/** - * Utility method that sorts an object array and keeping the order of equal items. - * ECMA script standard does not specify the behaviour when items are equal. - */ -function stableSort(arr, sortFunction) { - var length = arr.length, - sortValue, - i; - - // Add index to each item - for (i = 0; i < length; i++) { - arr[i].ss_i = i; // stable sort index - } - - arr.sort(function (a, b) { - sortValue = sortFunction(a, b); - return sortValue === 0 ? a.ss_i - b.ss_i : sortValue; - }); - - // Remove index from items - for (i = 0; i < length; i++) { - delete arr[i].ss_i; // stable sort index - } -} - -/** - * Non-recursive method to find the lowest member of an array. Math.min raises a maximum - * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This - * method is slightly slower, but safe. - */ -function arrayMin(data) { - var i = data.length, - min = data[0]; - - while (i--) { - if (data[i] < min) { - min = data[i]; - } - } - return min; -} - -/** - * Non-recursive method to find the lowest member of an array. Math.min raises a maximum - * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This - * method is slightly slower, but safe. - */ -function arrayMax(data) { - var i = data.length, - max = data[0]; - - while (i--) { - if (data[i] > max) { - max = data[i]; - } - } - return max; -} - -/** - * Utility method that destroys any SVGElement or VMLElement that are properties on the given object. - * It loops all properties and invokes destroy if there is a destroy method. The property is - * then delete'ed. - * @param {Object} The object to destroy properties on - * @param {Object} Exception, do not destroy this property, only delete it. - */ -function destroyObjectProperties(obj, except) { - var n; - for (n in obj) { - // If the object is non-null and destroy is defined - if (obj[n] && obj[n] !== except && obj[n].destroy) { - // Invoke the destroy - obj[n].destroy(); - } - - // Delete the property from the object. - delete obj[n]; - } -} - - -/** - * Discard an element by moving it to the bin and delete - * @param {Object} The HTML node to discard - */ -function discardElement(element) { - // create a garbage bin element, not part of the DOM - if (!garbageBin) { - garbageBin = createElement(DIV); - } - - // move the node and empty bin - if (element) { - garbageBin.appendChild(element); - } - garbageBin.innerHTML = ''; -} - -/** - * Provide error messages for debugging, with links to online explanation - */ -function error(code, stop) { - var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code; - if (stop) { - throw msg; - } else if (win.console) { - console.log(msg); - } -} - -/** - * Fix JS round off float errors - * @param {Number} num - */ -function correctFloat(num) { - return parseFloat( - num.toPrecision(14) - ); -} - -/** - * The time unit lookup - */ -/*jslint white: true*/ -timeUnits = hash( - MILLISECOND, 1, - SECOND, 1000, - MINUTE, 60000, - HOUR, 3600000, - DAY, 24 * 3600000, - WEEK, 7 * 24 * 3600000, - MONTH, 30 * 24 * 3600000, - YEAR, 31556952000 -); -/*jslint white: false*/ -/** - * Path interpolation algorithm used across adapters - */ -pathAnim = { - /** - * Prepare start and end values so that the path can be animated one to one - */ - init: function (elem, fromD, toD) { - fromD = fromD || ''; - var shift = elem.shift, - bezier = fromD.indexOf('C') > -1, - numParams = bezier ? 7 : 3, - endLength, - slice, - i, - start = fromD.split(' '), - end = [].concat(toD), // copy - startBaseLine, - endBaseLine, - sixify = function (arr) { // in splines make move points have six parameters like bezier curves - i = arr.length; - while (i--) { - if (arr[i] === M) { - arr.splice(i + 1, 0, arr[i + 1], arr[i + 2], arr[i + 1], arr[i + 2]); - } - } - }; - - if (bezier) { - sixify(start); - sixify(end); - } - - // pull out the base lines before padding - if (elem.isArea) { - startBaseLine = start.splice(start.length - 6, 6); - endBaseLine = end.splice(end.length - 6, 6); - } - - // if shifting points, prepend a dummy point to the end path - if (shift <= end.length / numParams) { - while (shift--) { - end = [].concat(end).splice(0, numParams).concat(end); - } - } - elem.shift = 0; // reset for following animations - - // copy and append last point until the length matches the end length - if (start.length) { - endLength = end.length; - while (start.length < endLength) { - - //bezier && sixify(start); - slice = [].concat(start).splice(start.length - numParams, numParams); - if (bezier) { // disable first control point - slice[numParams - 6] = slice[numParams - 2]; - slice[numParams - 5] = slice[numParams - 1]; - } - start = start.concat(slice); - } - } - - if (startBaseLine) { // append the base lines for areas - start = start.concat(startBaseLine); - end = end.concat(endBaseLine); - } - return [start, end]; - }, - - /** - * Interpolate each value of the path and return the array - */ - step: function (start, end, pos, complete) { - var ret = [], - i = start.length, - startVal; - - if (pos === 1) { // land on the final path without adjustment points appended in the ends - ret = complete; - - } else if (i === end.length && pos < 1) { - while (i--) { - startVal = parseFloat(start[i]); - ret[i] = - isNaN(startVal) ? // a letter instruction like M or L - start[i] : - pos * (parseFloat(end[i] - startVal)) + startVal; - - } - } else { // if animation is finished or length not matching, land on right value - ret = end; - } - return ret; - } -}; - - -/** - * Set the global animation to either a given value, or fall back to the - * given chart's animation option - * @param {Object} animation - * @param {Object} chart - */ -function setAnimation(animation, chart) { - globalAnimation = pick(animation, chart.animation); -} - - - -// check for a custom HighchartsAdapter defined prior to this file -var globalAdapter = win.HighchartsAdapter, - adapter = globalAdapter || {}, - - // Utility functions. If the HighchartsAdapter is not defined, adapter is an empty object - // and all the utility functions will be null. In that case they are populated by the - // default adapters below. - adapterRun = adapter.adapterRun, - getScript = adapter.getScript, - each = adapter.each, - grep = adapter.grep, - offset = adapter.offset, - map = adapter.map, - merge = adapter.merge, - addEvent = adapter.addEvent, - removeEvent = adapter.removeEvent, - fireEvent = adapter.fireEvent, - washMouseEvent = adapter.washMouseEvent, - animate = adapter.animate, - stop = adapter.stop; - -/* - * Define the adapter for frameworks. If an external adapter is not defined, - * Highcharts reverts to the built-in jQuery adapter. - */ -if (globalAdapter && globalAdapter.init) { - // Initialize the adapter with the pathAnim object that takes care - // of path animations. - globalAdapter.init(pathAnim); -} -if (!globalAdapter && win.jQuery) { - var jQ = jQuery; - - /** - * Downloads a script and executes a callback when done. - * @param {String} scriptLocation - * @param {Function} callback - */ - getScript = jQ.getScript; - - /** - * A direct link to jQuery methods. MooTools and Prototype adapters must be implemented for each case of method. - * @param {Object} elem The HTML element - * @param {String} method Which method to run on the wrapped element - */ - adapterRun = function (elem, method) { - return jQ(elem)[method](); - }; - - /** - * Utility for iterating over an array. Parameters are reversed compared to jQuery. - * @param {Array} arr - * @param {Function} fn - */ - each = function (arr, fn) { - var i = 0, - len = arr.length; - for (; i < len; i++) { - if (fn.call(arr[i], arr[i], i, arr) === false) { - return i; - } - } - }; - - /** - * Filter an array - */ - grep = jQ.grep; - - /** - * Map an array - * @param {Array} arr - * @param {Function} fn - */ - map = function (arr, fn) { - //return jQuery.map(arr, fn); - var results = [], - i = 0, - len = arr.length; - for (; i < len; i++) { - results[i] = fn.call(arr[i], arr[i], i, arr); - } - return results; - - }; - - /** - * Deep merge two objects and return a third object - */ - merge = function () { - var args = arguments; - return jQ.extend(true, null, args[0], args[1], args[2], args[3]); - }; - - /** - * Get the position of an element relative to the top left of the page - */ - offset = function (el) { - return jQ(el).offset(); - }; - - /** - * Add an event listener - * @param {Object} el A HTML element or custom object - * @param {String} event The event type - * @param {Function} fn The event handler - */ - addEvent = function (el, event, fn) { - jQ(el).bind(event, fn); - }; - - /** - * Remove event added with addEvent - * @param {Object} el The object - * @param {String} eventType The event type. Leave blank to remove all events. - * @param {Function} handler The function to remove - */ - removeEvent = function (el, eventType, handler) { - // workaround for jQuery issue with unbinding custom events: - // http://forum.jquery.com/topic/javascript-error-when-unbinding-a-custom-event-using-jquery-1-4-2 - var func = doc.removeEventListener ? 'removeEventListener' : 'detachEvent'; - if (doc[func] && !el[func]) { - el[func] = function () {}; - } - - jQ(el).unbind(eventType, handler); - }; - - /** - * Fire an event on a custom object - * @param {Object} el - * @param {String} type - * @param {Object} eventArguments - * @param {Function} defaultFunction - */ - fireEvent = function (el, type, eventArguments, defaultFunction) { - var event = jQ.Event(type), - detachedType = 'detached' + type, - defaultPrevented; - - // Remove warnings in Chrome when accessing layerX and layerY. Although Highcharts - // never uses these properties, Chrome includes them in the default click event and - // raises the warning when they are copied over in the extend statement below. - // - // To avoid problems in IE (see #1010) where we cannot delete the properties and avoid - // testing if they are there (warning in chrome) the only option is to test if running IE. - if (!isIE && eventArguments) { - delete eventArguments.layerX; - delete eventArguments.layerY; - } - - extend(event, eventArguments); - - // Prevent jQuery from triggering the object method that is named the - // same as the event. For example, if the event is 'select', jQuery - // attempts calling el.select and it goes into a loop. - if (el[type]) { - el[detachedType] = el[type]; - el[type] = null; - } - - // Wrap preventDefault and stopPropagation in try/catch blocks in - // order to prevent JS errors when cancelling events on non-DOM - // objects. #615. - each(['preventDefault', 'stopPropagation'], function (fn) { - var base = event[fn]; - event[fn] = function () { - try { - base.call(event); - } catch (e) { - if (fn === 'preventDefault') { - defaultPrevented = true; - } - } - }; - }); - - // trigger it - jQ(el).trigger(event); - - // attach the method - if (el[detachedType]) { - el[type] = el[detachedType]; - el[detachedType] = null; - } - - if (defaultFunction && !event.isDefaultPrevented() && !defaultPrevented) { - defaultFunction(event); - } - }; - - /** - * Extension method needed for MooTools - */ - washMouseEvent = function (e) { - return e; - }; - - /** - * Animate a HTML element or SVG element wrapper - * @param {Object} el - * @param {Object} params - * @param {Object} options jQuery-like animation options: duration, easing, callback - */ - animate = function (el, params, options) { - var $el = jQ(el); - if (params.d) { - el.toD = params.d; // keep the array form for paths, used in jQ.fx.step.d - params.d = 1; // because in jQuery, animating to an array has a different meaning - } - - $el.stop(); - $el.animate(params, options); - - }; - /** - * Stop running animation - */ - stop = function (el) { - jQ(el).stop(); - }; - - - //=== Extend jQuery on init - - /*jslint unparam: true*//* allow unused param x in this function */ - jQ.extend(jQ.easing, { - easeOutQuad: function (x, t, b, c, d) { - return -c * (t /= d) * (t - 2) + b; - } - }); - /*jslint unparam: false*/ - - // extend the animate function to allow SVG animations - var jFx = jQ.fx, - jStep = jFx.step; - - // extend some methods to check for elem.attr, which means it is a Highcharts SVG object - each(['cur', '_default', 'width', 'height'], function (fn, i) { - var obj = jStep, - base, - elem; - - // Handle different parent objects - if (fn === 'cur') { - obj = jFx.prototype; // 'cur', the getter, relates to jFx.prototype - - } else if (fn === '_default' && jQ.Tween) { // jQuery 1.8 model - obj = jQ.Tween.propHooks[fn]; - fn = 'set'; - } - - // Overwrite the method - base = obj[fn]; - if (base) { // step.width and step.height don't exist in jQuery < 1.7 - - // create the extended function replacement - obj[fn] = function (fx) { - - // jFx.prototype.cur does not use fx argument - fx = i ? fx : this; - - // shortcut - elem = fx.elem; - - // jFX.prototype.cur returns the current value. The other ones are setters - // and returning a value has no effect. - return elem.attr ? // is SVG element wrapper - elem.attr(fx.prop, fn === 'cur' ? UNDEFINED : fx.now) : // apply the SVG wrapper's method - base.apply(this, arguments); // use jQuery's built-in method - }; - } - }); - - // animate paths - jStep.d = function (fx) { - var elem = fx.elem; - - - // Normally start and end should be set in state == 0, but sometimes, - // for reasons unknown, this doesn't happen. Perhaps state == 0 is skipped - // in these cases - if (!fx.started) { - var ends = pathAnim.init(elem, elem.d, elem.toD); - fx.start = ends[0]; - fx.end = ends[1]; - fx.started = true; - } - - - // interpolate each value of the path - elem.attr('d', pathAnim.step(fx.start, fx.end, fx.pos, elem.toD)); - - }; -} - -/* **************************************************************************** - * Handle the options * - *****************************************************************************/ -var - -defaultLabelOptions = { - enabled: true, - // rotation: 0, - align: 'center', - x: 0, - y: 15, - /*formatter: function () { - return this.value; - },*/ - style: { - color: '#666', - fontSize: '11px', - lineHeight: '14px' - } -}; - -defaultOptions = { - colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE', - '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92'], - symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'], - lang: { - loading: 'Loading...', - months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', - 'August', 'September', 'October', 'November', 'December'], - shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], - weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], - decimalPoint: '.', - resetZoom: 'Reset zoom', - resetZoomTitle: 'Reset zoom level 1:1', - thousandsSep: ',' - }, - global: { - useUTC: true, - canvasToolsURL: 'http://code.highcharts.com/2.2.5/modules/canvas-tools.js' - }, - chart: { - //animation: true, - //alignTicks: false, - //reflow: true, - //className: null, - //events: { load, selection }, - //margin: [null], - //marginTop: null, - //marginRight: null, - //marginBottom: null, - //marginLeft: null, - borderColor: '#4572A7', - //borderWidth: 0, - borderRadius: 5, - defaultSeriesType: 'line', - ignoreHiddenSeries: true, - //inverted: false, - //shadow: false, - spacingTop: 10, - spacingRight: 10, - spacingBottom: 15, - spacingLeft: 10, - style: { - fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font - fontSize: '12px' - }, - backgroundColor: '#FFFFFF', - //plotBackgroundColor: null, - plotBorderColor: '#C0C0C0', - //plotBorderWidth: 0, - //plotShadow: false, - //zoomType: '' - resetZoomButton: { - theme: { - zIndex: 20 - }, - position: { - align: 'right', - x: -10, - //verticalAlign: 'top', - y: 10 - } - // relativeTo: 'plot' - } - }, - title: { - text: 'Chart title', - align: 'center', - // floating: false, - // margin: 15, - // x: 0, - // verticalAlign: 'top', - y: 15, - style: { - color: '#3E576F', - fontSize: '16px' - } - - }, - subtitle: { - text: '', - align: 'center', - // floating: false - // x: 0, - // verticalAlign: 'top', - y: 30, - style: { - color: '#6D869F' - } - }, - - plotOptions: { - line: { // base series options - allowPointSelect: false, - showCheckbox: false, - animation: { - duration: 1000 - }, - //connectNulls: false, - //cursor: 'default', - //clip: true, - //dashStyle: null, - //enableMouseTracking: true, - events: {}, - //legendIndex: 0, - lineWidth: 2, - shadow: true, - // stacking: null, - marker: { - enabled: true, - //symbol: null, - lineWidth: 0, - radius: 4, - lineColor: '#FFFFFF', - //fillColor: null, - states: { // states for a single point - hover: { - //radius: base + 2 - }, - select: { - fillColor: '#FFFFFF', - lineColor: '#000000', - lineWidth: 2 - } - } - }, - point: { - events: {} - }, - dataLabels: merge(defaultLabelOptions, { - enabled: false, - y: -6, - formatter: function () { - return this.y; - } - // backgroundColor: undefined, - // borderColor: undefined, - // borderRadius: undefined, - // borderWidth: undefined, - // padding: 3, - // shadow: false - }), - cropThreshold: 300, // draw points outside the plot area when the number of points is less than this - pointRange: 0, - //pointStart: 0, - //pointInterval: 1, - showInLegend: true, - states: { // states for the entire series - hover: { - //enabled: false, - //lineWidth: base + 1, - marker: { - // lineWidth: base + 1, - // radius: base + 1 - } - }, - select: { - marker: {} - } - }, - stickyTracking: true - //tooltip: { - //pointFormat: '{series.name}: {point.y}' - //valueDecimals: null, - //xDateFormat: '%A, %b %e, %Y', - //valuePrefix: '', - //ySuffix: '' - //} - // turboThreshold: 1000 - // zIndex: null - } - }, - labels: { - //items: [], - style: { - //font: defaultFont, - position: ABSOLUTE, - color: '#3E576F' - } - }, - legend: { - enabled: true, - align: 'center', - //floating: false, - layout: 'horizontal', - labelFormatter: function () { - return this.name; - }, - borderWidth: 1, - borderColor: '#909090', - borderRadius: 5, - navigation: { - // animation: true, - activeColor: '#3E576F', - // arrowSize: 12 - inactiveColor: '#CCC' - // style: {} // text styles - }, - // margin: 10, - // reversed: false, - shadow: false, - // backgroundColor: null, - /*style: { - padding: '5px' - },*/ - itemStyle: { - cursor: 'pointer', - color: '#3E576F', - fontSize: '12px' - }, - itemHoverStyle: { - //cursor: 'pointer', removed as of #601 - color: '#000' - }, - itemHiddenStyle: { - color: '#CCC' - }, - itemCheckboxStyle: { - position: ABSOLUTE, - width: '13px', // for IE precision - height: '13px' - }, - // itemWidth: undefined, - symbolWidth: 16, - symbolPadding: 5, - verticalAlign: 'bottom', - // width: undefined, - x: 0, - y: 0 - }, - - loading: { - // hideDuration: 100, - labelStyle: { - fontWeight: 'bold', - position: RELATIVE, - top: '1em' - }, - // showDuration: 0, - style: { - position: ABSOLUTE, - backgroundColor: 'white', - opacity: 0.5, - textAlign: 'center' - } - }, - - tooltip: { - enabled: true, - //crosshairs: null, - backgroundColor: 'rgba(255, 255, 255, .85)', - borderWidth: 2, - borderRadius: 5, - dateTimeLabelFormats: { - millisecond: '%A, %b %e, %H:%M:%S.%L', - second: '%A, %b %e, %H:%M:%S', - minute: '%A, %b %e, %H:%M', - hour: '%A, %b %e, %H:%M', - day: '%A, %b %e, %Y', - week: 'Week from %A, %b %e, %Y', - month: '%B %Y', - year: '%Y' - }, - //formatter: defaultFormatter, - headerFormat: '{point.key}
', - pointFormat: '{series.name}: {point.y}
', - shadow: true, - shared: useCanVG, - snap: hasTouch ? 25 : 10, - style: { - color: '#333333', - fontSize: '12px', - padding: '5px', - whiteSpace: 'nowrap' - } - //xDateFormat: '%A, %b %e, %Y', - //valueDecimals: null, - //valuePrefix: '', - //valueSuffix: '' - }, - - credits: { - enabled: true, - text: 'Highcharts.com', - href: 'http://www.highcharts.com', - position: { - align: 'right', - x: -10, - verticalAlign: 'bottom', - y: -5 - }, - style: { - cursor: 'pointer', - color: '#909090', - fontSize: '10px' - } - } -}; - - - - -// Series defaults -var defaultPlotOptions = defaultOptions.plotOptions, - defaultSeriesOptions = defaultPlotOptions.line; - -// set the default time methods -setTimeMethods(); - - - -/** - * Set the time methods globally based on the useUTC option. Time method can be either - * local time or UTC (default). - */ -function setTimeMethods() { - var useUTC = defaultOptions.global.useUTC, - GET = useUTC ? 'getUTC' : 'get', - SET = useUTC ? 'setUTC' : 'set'; - - makeTime = useUTC ? Date.UTC : function (year, month, date, hours, minutes, seconds) { - return new Date( - year, - month, - pick(date, 1), - pick(hours, 0), - pick(minutes, 0), - pick(seconds, 0) - ).getTime(); - }; - getMinutes = GET + 'Minutes'; - getHours = GET + 'Hours'; - getDay = GET + 'Day'; - getDate = GET + 'Date'; - getMonth = GET + 'Month'; - getFullYear = GET + 'FullYear'; - setMinutes = SET + 'Minutes'; - setHours = SET + 'Hours'; - setDate = SET + 'Date'; - setMonth = SET + 'Month'; - setFullYear = SET + 'FullYear'; - -} - -/** - * Merge the default options with custom options and return the new options structure - * @param {Object} options The new custom options - */ -function setOptions(options) { - - // Pull out axis options and apply them to the respective default axis options - /*defaultXAxisOptions = merge(defaultXAxisOptions, options.xAxis); - defaultYAxisOptions = merge(defaultYAxisOptions, options.yAxis); - options.xAxis = options.yAxis = UNDEFINED;*/ - - // Merge in the default options - defaultOptions = merge(defaultOptions, options); - - // Apply UTC - setTimeMethods(); - - return defaultOptions; -} - -/** - * Get the updated default options. Merely exposing defaultOptions for outside modules - * isn't enough because the setOptions method creates a new object. - */ -function getOptions() { - return defaultOptions; -} - - - -/** - * Handle color operations. The object methods are chainable. - * @param {String} input The input color in either rbga or hex format - */ -var Color = function (input) { - // declare variables - var rgba = [], result; - - /** - * Parse the input color to rgba array - * @param {String} input - */ - function init(input) { - - // rgba - result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/.exec(input); - if (result) { - rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)]; - } else { // hex - result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(input); - if (result) { - rgba = [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1]; - } - } - - } - /** - * Return the color a specified format - * @param {String} format - */ - function get(format) { - var ret; - - // it's NaN if gradient colors on a column chart - if (rgba && !isNaN(rgba[0])) { - if (format === 'rgb') { - ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')'; - } else if (format === 'a') { - ret = rgba[3]; - } else { - ret = 'rgba(' + rgba.join(',') + ')'; - } - } else { - ret = input; - } - return ret; - } - - /** - * Brighten the color - * @param {Number} alpha - */ - function brighten(alpha) { - if (isNumber(alpha) && alpha !== 0) { - var i; - for (i = 0; i < 3; i++) { - rgba[i] += pInt(alpha * 255); - - if (rgba[i] < 0) { - rgba[i] = 0; - } - if (rgba[i] > 255) { - rgba[i] = 255; - } - } - } - return this; - } - /** - * Set the color's opacity to a given alpha value - * @param {Number} alpha - */ - function setOpacity(alpha) { - rgba[3] = alpha; - return this; - } - - // initialize: parse the input - init(input); - - // public methods - return { - get: get, - brighten: brighten, - setOpacity: setOpacity - }; -}; - - -/** - * A wrapper object for SVG elements - */ -function SVGElement() {} - -SVGElement.prototype = { - /** - * Initialize the SVG renderer - * @param {Object} renderer - * @param {String} nodeName - */ - init: function (renderer, nodeName) { - var wrapper = this; - wrapper.element = nodeName === 'span' ? - createElement(nodeName) : - doc.createElementNS(SVG_NS, nodeName); - wrapper.renderer = renderer; - /** - * A collection of attribute setters. These methods, if defined, are called right before a certain - * attribute is set on an element wrapper. Returning false prevents the default attribute - * setter to run. Returning a value causes the default setter to set that value. Used in - * Renderer.label. - */ - wrapper.attrSetters = {}; - }, - /** - * Animate a given attribute - * @param {Object} params - * @param {Number} options The same options as in jQuery animation - * @param {Function} complete Function to perform at the end of animation - */ - animate: function (params, options, complete) { - var animOptions = pick(options, globalAnimation, true); - stop(this); // stop regardless of animation actually running, or reverting to .attr (#607) - if (animOptions) { - animOptions = merge(animOptions); - if (complete) { // allows using a callback with the global animation without overwriting it - animOptions.complete = complete; - } - animate(this, params, animOptions); - } else { - this.attr(params); - if (complete) { - complete(); - } - } - }, - /** - * Set or get a given attribute - * @param {Object|String} hash - * @param {Mixed|Undefined} val - */ - attr: function (hash, val) { - var wrapper = this, - key, - value, - result, - i, - child, - element = wrapper.element, - nodeName = element.nodeName, - renderer = wrapper.renderer, - skipAttr, - titleNode, - attrSetters = wrapper.attrSetters, - shadows = wrapper.shadows, - hasSetSymbolSize, - doTransform, - ret = wrapper; - - // single key-value pair - if (isString(hash) && defined(val)) { - key = hash; - hash = {}; - hash[key] = val; - } - - // used as a getter: first argument is a string, second is undefined - if (isString(hash)) { - key = hash; - if (nodeName === 'circle') { - key = { x: 'cx', y: 'cy' }[key] || key; - } else if (key === 'strokeWidth') { - key = 'stroke-width'; - } - ret = attr(element, key) || wrapper[key] || 0; - - if (key !== 'd' && key !== 'visibility') { // 'd' is string in animation step - ret = parseFloat(ret); - } - - // setter - } else { - - for (key in hash) { - skipAttr = false; // reset - value = hash[key]; - - // check for a specific attribute setter - result = attrSetters[key] && attrSetters[key](value, key); - - if (result !== false) { - if (result !== UNDEFINED) { - value = result; // the attribute setter has returned a new value to set - } - - // paths - if (key === 'd') { - if (value && value.join) { // join path - value = value.join(' '); - } - if (/(NaN| {2}|^$)/.test(value)) { - value = 'M 0 0'; - } - //wrapper.d = value; // shortcut for animations - - // update child tspans x values - } else if (key === 'x' && nodeName === 'text') { - for (i = 0; i < element.childNodes.length; i++) { - child = element.childNodes[i]; - // if the x values are equal, the tspan represents a linebreak - if (attr(child, 'x') === attr(element, 'x')) { - //child.setAttribute('x', value); - attr(child, 'x', value); - } - } - - if (wrapper.rotation) { - attr(element, 'transform', 'rotate(' + wrapper.rotation + ' ' + value + ' ' + - pInt(hash.y || attr(element, 'y')) + ')'); - } - - // apply gradients - } else if (key === 'fill') { - value = renderer.color(value, element, key); - - // circle x and y - } else if (nodeName === 'circle' && (key === 'x' || key === 'y')) { - key = { x: 'cx', y: 'cy' }[key] || key; - - // rectangle border radius - } else if (nodeName === 'rect' && key === 'r') { - attr(element, { - rx: value, - ry: value - }); - skipAttr = true; - - // translation and text rotation - } else if (key === 'translateX' || key === 'translateY' || key === 'rotation' || key === 'verticalAlign') { - doTransform = true; - skipAttr = true; - - // apply opacity as subnode (required by legacy WebKit and Batik) - } else if (key === 'stroke') { - value = renderer.color(value, element, key); - - // emulate VML's dashstyle implementation - } else if (key === 'dashstyle') { - key = 'stroke-dasharray'; - value = value && value.toLowerCase(); - if (value === 'solid') { - value = NONE; - } else if (value) { - value = value - .replace('shortdashdotdot', '3,1,1,1,1,1,') - .replace('shortdashdot', '3,1,1,1') - .replace('shortdot', '1,1,') - .replace('shortdash', '3,1,') - .replace('longdash', '8,3,') - .replace(/dot/g, '1,3,') - .replace('dash', '4,3,') - .replace(/,$/, '') - .split(','); // ending comma - - i = value.length; - while (i--) { - value[i] = pInt(value[i]) * hash['stroke-width']; - } - value = value.join(','); - } - - // special - } else if (key === 'isTracker') { - wrapper[key] = value; - - // IE9/MooTools combo: MooTools returns objects instead of numbers and IE9 Beta 2 - // is unable to cast them. Test again with final IE9. - } else if (key === 'width') { - value = pInt(value); - - // Text alignment - } else if (key === 'align') { - key = 'text-anchor'; - value = { left: 'start', center: 'middle', right: 'end' }[value]; - - // Title requires a subnode, #431 - } else if (key === 'title') { - titleNode = element.getElementsByTagName('title')[0]; - if (!titleNode) { - titleNode = doc.createElementNS(SVG_NS, 'title'); - element.appendChild(titleNode); - } - titleNode.textContent = value; - } - - // jQuery animate changes case - if (key === 'strokeWidth') { - key = 'stroke-width'; - } - - // Chrome/Win < 6 bug (http://code.google.com/p/chromium/issues/detail?id=15461) - if (isWebKit && key === 'stroke-width' && value === 0) { - value = 0.000001; - } - - // symbols - if (wrapper.symbolName && /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(key)) { - - - if (!hasSetSymbolSize) { - wrapper.symbolAttr(hash); - hasSetSymbolSize = true; - } - skipAttr = true; - } - - // let the shadow follow the main element - if (shadows && /^(width|height|visibility|x|y|d|transform)$/.test(key)) { - i = shadows.length; - while (i--) { - attr( - shadows[i], - key, - key === 'height' ? - mathMax(value - (shadows[i].cutHeight || 0), 0) : - value - ); - } - } - - // validate heights - if ((key === 'width' || key === 'height') && nodeName === 'rect' && value < 0) { - value = 0; - } - - // Record for animation and quick access without polling the DOM - wrapper[key] = value; - - // Update transform - if (doTransform) { - wrapper.updateTransform(); - } - - - if (key === 'text') { - // only one node allowed - wrapper.textStr = value; - if (wrapper.added) { - renderer.buildText(wrapper); - } - } else if (!skipAttr) { - attr(element, key, value); - } - - } - - } - - } - - // Workaround for our #732, WebKit's issue https://bugs.webkit.org/show_bug.cgi?id=78385 - // TODO: If the WebKit team fix this bug before the final release of Chrome 18, remove the workaround. - if (isWebKit && /Chrome\/(18|19)/.test(userAgent)) { - if (nodeName === 'text' && (hash.x !== UNDEFINED || hash.y !== UNDEFINED)) { - var parent = element.parentNode, - next = element.nextSibling; - - if (parent) { - parent.removeChild(element); - if (next) { - parent.insertBefore(element, next); - } else { - parent.appendChild(element); - } - } - } - } - // End of workaround for #732 - - return ret; - }, - - /** - * If one of the symbol size affecting parameters are changed, - * check all the others only once for each call to an element's - * .attr() method - * @param {Object} hash - */ - symbolAttr: function (hash) { - var wrapper = this; - - each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function (key) { - wrapper[key] = pick(hash[key], wrapper[key]); - }); - - wrapper.attr({ - d: wrapper.renderer.symbols[wrapper.symbolName](wrapper.x, wrapper.y, wrapper.width, wrapper.height, wrapper) - }); - }, - - /** - * Apply a clipping path to this object - * @param {String} id - */ - clip: function (clipRect) { - return this.attr('clip-path', 'url(' + this.renderer.url + '#' + clipRect.id + ')'); - }, - - /** - * Calculate the coordinates needed for drawing a rectangle crisply and return the - * calculated attributes - * @param {Number} strokeWidth - * @param {Number} x - * @param {Number} y - * @param {Number} width - * @param {Number} height - */ - crisp: function (strokeWidth, x, y, width, height) { - - var wrapper = this, - key, - attribs = {}, - values = {}, - normalizer; - - strokeWidth = strokeWidth || wrapper.strokeWidth || (wrapper.attr && wrapper.attr('stroke-width')) || 0; - normalizer = mathRound(strokeWidth) % 2 / 2; // mathRound because strokeWidth can sometimes have roundoff errors - - // normalize for crisp edges - values.x = mathFloor(x || wrapper.x || 0) + normalizer; - values.y = mathFloor(y || wrapper.y || 0) + normalizer; - values.width = mathFloor((width || wrapper.width || 0) - 2 * normalizer); - values.height = mathFloor((height || wrapper.height || 0) - 2 * normalizer); - values.strokeWidth = strokeWidth; - - for (key in values) { - if (wrapper[key] !== values[key]) { // only set attribute if changed - wrapper[key] = attribs[key] = values[key]; - } - } - - return attribs; - }, - - /** - * Set styles for the element - * @param {Object} styles - */ - css: function (styles) { - /*jslint unparam: true*//* allow unused param a in the regexp function below */ - var elemWrapper = this, - elem = elemWrapper.element, - textWidth = styles && styles.width && elem.nodeName === 'text', - n, - serializedCss = '', - hyphenate = function (a, b) { return '-' + b.toLowerCase(); }; - /*jslint unparam: false*/ - - // convert legacy - if (styles && styles.color) { - styles.fill = styles.color; - } - - // Merge the new styles with the old ones - styles = extend( - elemWrapper.styles, - styles - ); - - // store object - elemWrapper.styles = styles; - - // serialize and set style attribute - if (isIE && !hasSVG) { // legacy IE doesn't support setting style attribute - if (textWidth) { - delete styles.width; - } - css(elemWrapper.element, styles); - } else { - for (n in styles) { - serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';'; - } - elemWrapper.attr({ - style: serializedCss - }); - } - - - // re-build text - if (textWidth && elemWrapper.added) { - elemWrapper.renderer.buildText(elemWrapper); - } - - return elemWrapper; - }, - - /** - * Add an event listener - * @param {String} eventType - * @param {Function} handler - */ - on: function (eventType, handler) { - var fn = handler; - // touch - if (hasTouch && eventType === 'click') { - eventType = 'touchstart'; - fn = function (e) { - e.preventDefault(); - handler(); - }; - } - // simplest possible event model for internal use - this.element['on' + eventType] = fn; - return this; - }, - - /** - * Set the coordinates needed to draw a consistent radial gradient across - * pie slices regardless of positioning inside the chart. The format is - * [centerX, centerY, diameter] in pixels. - */ - setRadialReference: function (coordinates) { - this.element.radialReference = coordinates; - return this; - }, - - /** - * Move an object and its children by x and y values - * @param {Number} x - * @param {Number} y - */ - translate: function (x, y) { - return this.attr({ - translateX: x, - translateY: y - }); - }, - - /** - * Invert a group, rotate and flip - */ - invert: function () { - var wrapper = this; - wrapper.inverted = true; - wrapper.updateTransform(); - return wrapper; - }, - - /** - * Apply CSS to HTML elements. This is used in text within SVG rendering and - * by the VML renderer - */ - htmlCss: function (styles) { - var wrapper = this, - element = wrapper.element, - textWidth = styles && element.tagName === 'SPAN' && styles.width; - - if (textWidth) { - delete styles.width; - wrapper.textWidth = textWidth; - wrapper.updateTransform(); - } - - wrapper.styles = extend(wrapper.styles, styles); - css(wrapper.element, styles); - - return wrapper; - }, - - - - /** - * VML and useHTML method for calculating the bounding box based on offsets - * @param {Boolean} refresh Whether to force a fresh value from the DOM or to - * use the cached value - * - * @return {Object} A hash containing values for x, y, width and height - */ - - htmlGetBBox: function (refresh) { - var wrapper = this, - element = wrapper.element, - bBox = wrapper.bBox; - - // faking getBBox in exported SVG in legacy IE - if (!bBox || refresh) { - // 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; - }, - - /** - * VML override private method to update elements based on internal - * properties based on SVG transform - */ - htmlUpdateTransform: function () { - // aligning non added elements is expensive - if (!this.added) { - this.alignOnAdd = true; - return; - } - - var wrapper = this, - renderer = wrapper.renderer, - 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', - shadows = wrapper.shadows; - - // apply translate - if (translateX || translateY) { - css(elem, { - marginLeft: translateX, - marginTop: translateY - }); - if (shadows) { // used in labels/tooltip - each(shadows, function (shadow) { - css(shadow, { - marginLeft: translateX + 1, - marginTop: translateY + 1 - }); - }); - } - } - - // apply inversion - if (wrapper.inverted) { // wrapper is a group - each(elem.childNodes, function (child) { - renderer.invertChild(child, elem); - }); - } - - if (elem.tagName === 'SPAN') { - - var width, height, - rotation = wrapper.rotation, - baseline, - 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. Rotation of useHTML content is not yet implemented - // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+ - // has support for CSS3 transform. The getBBox method also needs to be updated - // to compensate for the rotation, like it currently does for SVG. - // 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 = pick(wrapper.elemWidth, elem.offsetWidth); - height = pick(wrapper.elemHeight, elem.offsetHeight); - - // update textWidth - if (width > textWidth && /[ \-]/.test(elem.innerText)) { // #983 - css(elem, { - width: textWidth + PX, - display: 'block', - whiteSpace: 'normal' - }); - width = textWidth; - } - - // correct x and y - baseline = renderer.fontMetrics(elem.style.fontSize).b; - xCorr = costheta < 0 && -width; - yCorr = sintheta < 0 && -height; - - // correct for baseline and corners spilling out after rotation - quad = costheta * sintheta < 0; - xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection); - yCorr -= costheta * baseline * (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) + PX, - top: (y + yCorr) + PX - }); - - // record current text transform - wrapper.cTT = currentTextTransform; - } - }, - - /** - * Private method to update the transform attribute based on internal - * properties - */ - updateTransform: function () { - var wrapper = this, - translateX = wrapper.translateX || 0, - translateY = wrapper.translateY || 0, - inverted = wrapper.inverted, - rotation = wrapper.rotation, - transform = []; - - // flipping affects translate as adjustment for flipping around the group's axis - if (inverted) { - translateX += wrapper.attr('width'); - translateY += wrapper.attr('height'); - } - - // apply translate - if (translateX || translateY) { - transform.push('translate(' + translateX + ',' + translateY + ')'); - } - - // apply rotation - if (inverted) { - transform.push('rotate(90) scale(-1,1)'); - } else if (rotation) { // text rotation - transform.push('rotate(' + rotation + ' ' + (wrapper.x || 0) + ' ' + (wrapper.y || 0) + ')'); - } - - if (transform.length) { - attr(wrapper.element, 'transform', transform.join(' ')); - } - }, - /** - * Bring the element to the front - */ - toFront: function () { - var element = this.element; - element.parentNode.appendChild(element); - return this; - }, - - - /** - * Break down alignment options like align, verticalAlign, x and y - * to x and y relative to the chart. - * - * @param {Object} alignOptions - * @param {Boolean} alignByTranslate - * @param {Object} box The box to align to, needs a width and height - * - */ - align: function (alignOptions, alignByTranslate, box) { - var elemWrapper = this; - - if (!alignOptions) { // called on resize - alignOptions = elemWrapper.alignOptions; - alignByTranslate = elemWrapper.alignByTranslate; - } else { // first call on instanciate - elemWrapper.alignOptions = alignOptions; - elemWrapper.alignByTranslate = alignByTranslate; - if (!box) { // boxes other than renderer handle this internally - elemWrapper.renderer.alignedObjects.push(elemWrapper); - } - } - - box = pick(box, elemWrapper.renderer); - - var align = alignOptions.align, - vAlign = alignOptions.verticalAlign, - x = (box.x || 0) + (alignOptions.x || 0), // default: left align - y = (box.y || 0) + (alignOptions.y || 0), // default: top align - attribs = {}; - - - // align - if (/^(right|center)$/.test(align)) { - x += (box.width - (alignOptions.width || 0)) / - { right: 1, center: 2 }[align]; - } - attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x); - - - // vertical align - if (/^(bottom|middle)$/.test(vAlign)) { - y += (box.height - (alignOptions.height || 0)) / - ({ bottom: 1, middle: 2 }[vAlign] || 1); - - } - attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y); - - // animate only if already placed - elemWrapper[elemWrapper.placed ? 'animate' : 'attr'](attribs); - elemWrapper.placed = true; - elemWrapper.alignAttr = attribs; - - return elemWrapper; - }, - - /** - * Get the bounding box (width, height, x and y) for the element - */ - getBBox: function (refresh) { - var wrapper = this, - bBox, - width, - height, - rotation = wrapper.rotation, - element = wrapper.element, - rad = rotation * deg2rad; - - // SVG elements - if (element.namespaceURI === SVG_NS || wrapper.renderer.forExport) { - try { // Fails in Firefox if the container has display: none. - - bBox = element.getBBox ? - // SVG: use extend because IE9 is not allowed to change width and height in case - // of rotation (below) - extend({}, element.getBBox()) : - // Canvas renderer and legacy IE in export mode - { - width: element.offsetWidth, - height: element.offsetHeight - }; - } catch (e) {} - - // If the bBox is not set, the try-catch block above failed. The other condition - // is for Opera that returns a width of -Infinity on hidden elements. - if (!bBox || bBox.width < 0) { - bBox = { width: 0, height: 0 }; - } - - width = bBox.width; - height = bBox.height; - - // adjust for rotated text - if (rotation) { - bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad)); - bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad)); - } - - // VML Renderer or useHTML within SVG - } else { - bBox = wrapper.htmlGetBBox(refresh); - } - - return bBox; - }, - - /** - * Show the element - */ - show: function () { - return this.attr({ visibility: VISIBLE }); - }, - - /** - * Hide the element - */ - hide: function () { - return this.attr({ visibility: HIDDEN }); - }, - - /** - * Add the element - * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined - * to append the element to the renderer.box. - */ - add: function (parent) { - - var renderer = this.renderer, - parentWrapper = parent || renderer, - parentNode = parentWrapper.element || renderer.box, - childNodes = parentNode.childNodes, - element = this.element, - zIndex = attr(element, 'zIndex'), - otherElement, - otherZIndex, - i, - inserted; - - // mark as inverted - this.parentInverted = parent && parent.inverted; - - // build formatted text - if (this.textStr !== undefined) { - renderer.buildText(this); - } - - // mark the container as having z indexed children - if (zIndex) { - parentWrapper.handleZ = true; - zIndex = pInt(zIndex); - } - - // insert according to this and other elements' zIndex - if (parentWrapper.handleZ) { // this element or any of its siblings has a z index - for (i = 0; i < childNodes.length; i++) { - otherElement = childNodes[i]; - otherZIndex = attr(otherElement, 'zIndex'); - if (otherElement !== element && ( - // insert before the first element with a higher zIndex - pInt(otherZIndex) > zIndex || - // if no zIndex given, insert before the first element with a zIndex - (!defined(zIndex) && defined(otherZIndex)) - - )) { - parentNode.insertBefore(element, otherElement); - inserted = true; - break; - } - } - } - - // default: append at the end - if (!inserted) { - parentNode.appendChild(element); - } - - // mark as added - this.added = true; - - // fire an event for internal hooks - fireEvent(this, 'add'); - - return this; - }, - - /** - * 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) { - var parentNode = element.parentNode; - if (parentNode) { - parentNode.removeChild(element); - } - }, - - /** - * Destroy the element and element wrapper - */ - destroy: function () { - var wrapper = this, - element = wrapper.element || {}, - shadows = wrapper.shadows, - box = wrapper.box, - key, - i; - - // remove events - element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = null; - stop(wrapper); // stop running animations - - if (wrapper.clipPath) { - wrapper.clipPath = wrapper.clipPath.destroy(); - } - - // Destroy stops in case this is a gradient object - if (wrapper.stops) { - for (i = 0; i < wrapper.stops.length; i++) { - wrapper.stops[i] = wrapper.stops[i].destroy(); - } - wrapper.stops = null; - } - - // remove element - wrapper.safeRemoveChild(element); - - // destroy shadows - if (shadows) { - each(shadows, function (shadow) { - wrapper.safeRemoveChild(shadow); - }); - } - - // destroy label box - if (box) { - box.destroy(); - } - - // remove from alignObjects - erase(wrapper.renderer.alignedObjects, wrapper); - - for (key in wrapper) { - delete wrapper[key]; - } - - return null; - }, - - /** - * Empty a group element - */ - empty: function () { - var element = this.element, - childNodes = element.childNodes, - i = childNodes.length; - - while (i--) { - element.removeChild(childNodes[i]); - } - }, - - /** - * Add a shadow to the element. Must be done after the element is added to the DOM - * @param {Boolean} apply - */ - shadow: function (apply, group, cutOff) { - var shadows = [], - i, - shadow, - element = this.element, - strokeWidth, - - // compensate for inverted plot area - transform = this.parentInverted ? '(-1,-1)' : '(1,1)'; - - - if (apply) { - for (i = 1; i <= 3; i++) { - shadow = element.cloneNode(0); - strokeWidth = 7 - 2 * i; - attr(shadow, { - 'isShadow': 'true', - 'stroke': 'rgb(0, 0, 0)', - 'stroke-opacity': 0.05 * i, - 'stroke-width': strokeWidth, - 'transform': 'translate' + transform, - 'fill': NONE - }); - if (cutOff) { - attr(shadow, 'height', mathMax(attr(shadow, 'height') - strokeWidth, 0)); - shadow.cutHeight = strokeWidth; - } - - if (group) { - group.element.appendChild(shadow); - } else { - element.parentNode.insertBefore(shadow, element); - } - - shadows.push(shadow); - } - - this.shadows = shadows; - } - return this; - - } -}; - - -/** - * The default SVG renderer - */ -var SVGRenderer = function () { - this.init.apply(this, arguments); -}; -SVGRenderer.prototype = { - Element: SVGElement, - - /** - * Initialize the SVGRenderer - * @param {Object} container - * @param {Number} width - * @param {Number} height - * @param {Boolean} forExport - */ - init: function (container, width, height, forExport) { - var renderer = this, - loc = location, - boxWrapper; - - boxWrapper = renderer.createElement('svg') - .attr({ - xmlns: SVG_NS, - version: '1.1' - }); - container.appendChild(boxWrapper.element); - - // object properties - renderer.isSVG = true; - renderer.box = boxWrapper.element; - renderer.boxWrapper = boxWrapper; - renderer.alignedObjects = []; - renderer.url = isIE ? '' : loc.href.replace(/#.*?$/, '') - .replace(/([\('\)])/g, '\\$1'); // Page url used for internal references. #24, #672. - renderer.defs = this.createElement('defs').add(); - renderer.forExport = forExport; - renderer.gradients = {}; // Object where gradient SvgElements are stored - - renderer.setSize(width, height, false); - - - - // 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) { - renderer.subPixelFix = subPixelFix = function () { - css(container, { left: 0, top: 0 }); - rect = container.getBoundingClientRect(); - css(container, { - left: (mathCeil(rect.left) - rect.left) + PX, - top: (mathCeil(rect.top) - rect.top) + PX - }); - }; - - // run the fix now - subPixelFix(); - - // run it on resize - addEvent(win, 'resize', subPixelFix); - } - }, - - /** - * Detect whether the renderer is hidden. This happens when one of the parent elements - * has display: none. #608. - */ - isHidden: function () { - return !this.boxWrapper.getBBox().width; - }, - - /** - * Destroys the renderer and its allocated members. - */ - destroy: function () { - var renderer = this, - rendererDefs = renderer.defs; - renderer.box = null; - renderer.boxWrapper = renderer.boxWrapper.destroy(); - - // Call destroy on all gradient elements - destroyObjectProperties(renderer.gradients || {}); - renderer.gradients = null; - - // Defs are null in VMLRenderer - // Otherwise, destroy them here. - if (rendererDefs) { - renderer.defs = rendererDefs.destroy(); - } - - // Remove sub pixel fix handler - // We need to check that there is a handler, otherwise all functions that are registered for event 'resize' are removed - // See issue #982 - if (renderer.subPixelFix) { - removeEvent(win, 'resize', renderer.subPixelFix); - } - - renderer.alignedObjects = null; - - return null; - }, - - /** - * Create a wrapper for an SVG element - * @param {Object} nodeName - */ - createElement: function (nodeName) { - var wrapper = new this.Element(); - wrapper.init(this, nodeName); - return wrapper; - }, - - /** - * Dummy function for use in canvas renderer - */ - draw: function () {}, - - /** - * Parse a simple HTML string into SVG tspans - * - * @param {Object} textNode The parent text SVG node - */ - buildText: function (wrapper) { - var textNode = wrapper.element, - lines = pick(wrapper.textStr, '').toString() - .replace(/<(b|strong)>/g, '') - .replace(/<(i|em)>/g, '') - .replace(//g, '') - .split(//g), - childNodes = textNode.childNodes, - styleRegex = /style="([^"]+)"/, - hrefRegex = /href="([^"]+)"/, - parentX = attr(textNode, 'x'), - textStyles = wrapper.styles, - width = textStyles && pInt(textStyles.width), - textLineHeight = textStyles && textStyles.lineHeight, - lastLine, - GET_COMPUTED_STYLE = 'getComputedStyle', - i = childNodes.length, - linePositions = []; - - // Needed in IE9 because it doesn't report tspan's offsetHeight (#893) - function getLineHeightByBBox(lineNo) { - linePositions[lineNo] = textNode.getBBox().height; - return mathRound(linePositions[lineNo] - (linePositions[lineNo - 1] || 0)); - } - - // 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 - } - - // remove empty line at end - if (lines[lines.length - 1] === '') { - lines.pop(); - } - - // build the lines - 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 || getLineHeightByBBox(lineNo) || 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 = wrapper.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, '-'))); - } - } - } - } - }); - }); - }, - - /** - * Create a button with preset states - * @param {String} text - * @param {Number} x - * @param {Number} y - * @param {Function} callback - * @param {Object} normalState - * @param {Object} hoverState - * @param {Object} pressedState - */ - button: function (text, x, y, callback, normalState, hoverState, pressedState) { - var label = this.label(text, x, y), - curState = 0, - stateOptions, - stateStyle, - normalStyle, - hoverStyle, - pressedStyle, - STYLE = 'style', - verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 }; - - // prepare the attributes - /*jslint white: true*/ - normalState = merge(hash( - STROKE_WIDTH, 1, - STROKE, '#999', - FILL, hash( - LINEAR_GRADIENT, verticalGradient, - STOPS, [ - [0, '#FFF'], - [1, '#DDD'] - ] - ), - 'r', 3, - 'padding', 3, - STYLE, hash( - 'color', 'black' - ) - ), normalState); - /*jslint white: false*/ - normalStyle = normalState[STYLE]; - delete normalState[STYLE]; - - /*jslint white: true*/ - hoverState = merge(normalState, hash( - STROKE, '#68A', - FILL, hash( - LINEAR_GRADIENT, verticalGradient, - STOPS, [ - [0, '#FFF'], - [1, '#ACF'] - ] - ) - ), hoverState); - /*jslint white: false*/ - hoverStyle = hoverState[STYLE]; - delete hoverState[STYLE]; - - /*jslint white: true*/ - pressedState = merge(normalState, hash( - STROKE, '#68A', - FILL, hash( - LINEAR_GRADIENT, verticalGradient, - STOPS, [ - [0, '#9BD'], - [1, '#CDF'] - ] - ) - ), pressedState); - /*jslint white: false*/ - pressedStyle = pressedState[STYLE]; - delete pressedState[STYLE]; - - // add the events - addEvent(label.element, 'mouseenter', function () { - label.attr(hoverState) - .css(hoverStyle); - }); - addEvent(label.element, 'mouseleave', function () { - stateOptions = [normalState, hoverState, pressedState][curState]; - stateStyle = [normalStyle, hoverStyle, pressedStyle][curState]; - label.attr(stateOptions) - .css(stateStyle); - }); - - label.setState = function (state) { - curState = state; - if (!state) { - label.attr(normalState) - .css(normalStyle); - } else if (state === 2) { - label.attr(pressedState) - .css(pressedStyle); - } - }; - - return label - .on('click', function () { - callback.call(label); - }) - .attr(normalState) - .css(extend({ cursor: 'default' }, normalStyle)); - }, - - /** - * 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) { - var attr = { - fill: NONE - }; - if (isArray(path)) { - attr.d = path; - } else if (isObject(path)) { // attributes - extend(attr, path); - } - return this.createElement('path').attr(attr); - }, - - /** - * 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, 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) { - - r = isObject(x) ? x.r : r; - - var wrapper = this.createElement('rect').attr({ - rx: r, - ry: r, - fill: NONE - }); - return wrapper.attr( - isObject(x) ? - x : - // do not crispify when an object is passed in (as in column charts) - 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, width, height, 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), - width, - height, - options - ), - - imageRegex = /^url\((.*?)\)$/, - imageSrc, - imageSize, - centerImage; - - if (path) { - - obj = this.path(path); - // expando properties for use in animate and attr - extend(obj, { - symbolName: symbol, - x: x, - y: y, - width: width, - height: height - }); - if (options) { - extend(obj, options); - } - - - // image symbols - } else if (imageRegex.test(symbol)) { - - // On image load, set the size and position - centerImage = function (img, size) { - img.attr({ - width: size[0], - height: size[1] - }); - - if (!img.alignByTranslate) { // #185 - img.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 - }); - } - } - - return obj; - }, - - /** - * An extendable collection of functions for defining symbol paths. - */ - symbols: { - 'circle': function (x, y, w, h) { - var cpw = 0.166 * w; - return [ - M, x + w / 2, y, - 'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h, - 'C', x - cpw, y + h, x - cpw, y, x + w / 2, y, - 'Z' - ]; - }, - - 'square': function (x, y, w, h) { - return [ - M, x, y, - L, x + w, y, - x + w, y + h, - x, y + h, - 'Z' - ]; - }, - - 'triangle': function (x, y, w, h) { - return [ - M, x + w / 2, y, - L, x + w, y + h, - x, y + h, - 'Z' - ]; - }, - - 'triangle-down': function (x, y, w, h) { - return [ - M, x, y, - L, x + w, y, - x + w / 2, y + h, - 'Z' - ]; - }, - 'diamond': function (x, y, w, h) { - return [ - M, x + w / 2, y, - L, x + w, y + h / 2, - x + w / 2, y + h, - x, y + h / 2, - 'Z' - ]; - }, - 'arc': function (x, y, w, h, options) { - var start = options.start, - radius = options.r || w || h, - end = options.end - 0.000001, // to prevent cos and sin of start and end from becoming equal on 360 arcs - innerRadius = options.innerR, - open = options.open, - 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, - open ? M : 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, - - open ? '' : '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. Prior to Highstock, an array was used to define - * a linear gradient with pixel positions relative to the SVG. In newer versions - * we change the coordinates to apply relative to the shape, using coordinates - * 0-1 within the shape. To preserve backwards compatibility, linearGradient - * in this definition is an object of x1, y1, x2 and y2. - * - * @param {Object} color The color or config object - */ - color: function (color, elem, prop) { - var renderer = this, - colorObject, - regexRgba = /^rgba/, - gradName; - - // Apply linear or radial gradients - if (color && color.linearGradient) { - gradName = 'linearGradient'; - } else if (color && color.radialGradient) { - gradName = 'radialGradient'; - } - - if (gradName) { - var gradAttr = color[gradName], - gradients = renderer.gradients, - gradientObject, - stopColor, - stopOpacity, - radialReference = elem.radialReference; - - // Check if a gradient object with the same config object is created within this renderer - if (!gradAttr.id || !gradients[gradAttr.id]) { - - // Keep < 2.2 kompatibility - if (isArray(gradAttr)) { - color[gradName] = gradAttr = { - x1: gradAttr[0], - y1: gradAttr[1], - x2: gradAttr[2], - y2: gradAttr[3], - gradientUnits: 'userSpaceOnUse' - }; - } - - // Correct the radial gradient for the radial reference system - if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) { - extend(gradAttr, { - cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2], - cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2], - r: gradAttr.r * radialReference[2], - gradientUnits: 'userSpaceOnUse' - }); - } - - // Set the id and create the element - gradAttr.id = PREFIX + idCounter++; - gradients[gradAttr.id] = gradientObject = renderer.createElement(gradName) - .attr(gradAttr) - .add(renderer.defs); - - - // 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 the reference to the gradient object - return 'url(' + renderer.url + '#' + gradAttr.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 renderer = this, - defaultChartStyle = defaultOptions.chart.style, - wrapper; - - if (useHTML && !renderer.forExport) { - return renderer.html(str, x, y); - } - - x = mathRound(pick(x, 0)); - y = mathRound(pick(y, 0)); - - wrapper = renderer.createElement('text') - .attr({ - x: x, - y: y, - text: str - }) - .css({ - fontFamily: defaultChartStyle.fontFamily, - fontSize: defaultChartStyle.fontSize - }); - - wrapper.x = x; - wrapper.y = y; - return wrapper; - }, - - - /** - * Create HTML text node. This is used by the VML renderer as well as the SVG - * renderer through the useHTML option. - * - * @param {String} str - * @param {Number} x - * @param {Number} y - */ - html: function (str, x, y) { - var defaultChartStyle = defaultOptions.chart.style, - wrapper = this.createElement('span'), - attrSetters = wrapper.attrSetters, - element = wrapper.element, - renderer = wrapper.renderer; - - // Text setter - attrSetters.text = function (value) { - element.innerHTML = value; - return false; - }; - - // Various setters which rely on update transform - attrSetters.x = attrSetters.y = attrSetters.align = function (value, key) { - if (key === 'align') { - key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML. - } - wrapper[key] = value; - wrapper.htmlUpdateTransform(); - return false; - }; - - // Set the default attributes - wrapper.attr({ - text: str, - x: mathRound(x), - y: mathRound(y) - }) - .css({ - position: ABSOLUTE, - whiteSpace: 'nowrap', - fontFamily: defaultChartStyle.fontFamily, - fontSize: defaultChartStyle.fontSize - }); - - // Use the HTML specific .css method - wrapper.css = wrapper.htmlCss; - - // This is specific for HTML within SVG - if (renderer.isSVG) { - wrapper.add = function (svgGroupWrapper) { - - var htmlGroup, - htmlGroupStyle, - container = renderer.box.parentNode; - - // Create a mock group to hold the HTML elements - if (svgGroupWrapper) { - htmlGroup = svgGroupWrapper.div; - if (!htmlGroup) { - htmlGroup = svgGroupWrapper.div = createElement(DIV, { - className: attr(svgGroupWrapper.element, 'class') - }, { - position: ABSOLUTE, - left: svgGroupWrapper.attr('translateX') + PX, - top: svgGroupWrapper.attr('translateY') + PX - }, container); - - // Ensure dynamic updating position - htmlGroupStyle = htmlGroup.style; - extend(svgGroupWrapper.attrSetters, { - translateX: function (value) { - htmlGroupStyle.left = value + PX; - }, - translateY: function (value) { - htmlGroupStyle.top = value + PX; - }, - visibility: function (value, key) { - htmlGroupStyle[key] = value; - } - }); - - } - } else { - htmlGroup = container; - } - - htmlGroup.appendChild(element); - - // Shared with VML: - wrapper.added = true; - if (wrapper.alignOnAdd) { - wrapper.htmlUpdateTransform(); - } - - return wrapper; - }; - } - return wrapper; - }, - - /** - * Utility to return the baseline offset and total line height from the font size - */ - fontMetrics: function (fontSize) { - fontSize = pInt(fontSize || 11); - - // Empirical values found by comparing font size and bounding box height. - // Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/ - var lineHeight = fontSize < 24 ? fontSize + 4 : mathRound(fontSize * 1.2), - baseline = mathRound(lineHeight * 0.8); - - return { - h: lineHeight, - b: baseline - }; - }, - - /** - * Add a label, a text item that can hold a colored or gradient background - * as well as a border and shadow. - * @param {string} str - * @param {Number} x - * @param {Number} y - * @param {String} shape - * @param {Number} anchorX In case the shape has a pointer, like a flag, this is the - * coordinates it should be pinned to - * @param {Number} anchorY - * @param {Boolean} baseline Whether to position the label relative to the text baseline, - * like renderer.text, or to the upper border of the rectangle. - * @param {String} className Class name for the group - */ - label: function (str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) { - - var renderer = this, - wrapper = renderer.g(className), - text = renderer.text('', 0, 0, useHTML) - .attr({ - zIndex: 1 - }) - .add(wrapper), - box, - bBox, - alignFactor = 0, - padding = 3, - width, - height, - wrapperX, - wrapperY, - crispAdjust = 0, - deferredAttr = {}, - baselineOffset, - attrSetters = wrapper.attrSetters; - - /** - * This function runs after the label is added to the DOM (when the bounding box is - * available), and after the text of the label is updated to detect the new bounding - * box and reflect it in the border box. - */ - function updateBoxSize() { - var boxY, - style = text.element.style; - - bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && - text.getBBox(true); - wrapper.width = (width || bBox.width || 0) + 2 * padding; - wrapper.height = (height || bBox.height || 0) + 2 * padding; - - // update the label-scoped y offset - baselineOffset = padding + renderer.fontMetrics(style && style.fontSize).b; - - - // create the border box if it is not already present - if (!box) { - boxY = baseline ? -baselineOffset : 0; - - wrapper.box = box = shape ? - renderer.symbol(shape, -alignFactor * padding, boxY, wrapper.width, wrapper.height) : - renderer.rect(-alignFactor * padding, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]); - box.add(wrapper); - } - - // apply the box attributes - box.attr(merge({ - width: wrapper.width, - height: wrapper.height - }, deferredAttr)); - deferredAttr = null; - } - - /** - * This function runs after setting text or padding, but only if padding is changed - */ - function updateTextPadding() { - var styles = wrapper.styles, - textAlign = styles && styles.textAlign, - x = padding * (1 - alignFactor), - y; - - // determin y based on the baseline - y = baseline ? 0 : baselineOffset; - - // compensate for alignment - if (defined(width) && (textAlign === 'center' || textAlign === 'right')) { - x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width); - } - - // update if anything changed - if (x !== text.x || y !== text.y) { - text.attr({ - x: x, - y: y - }); - } - - // record current values - text.x = x; - text.y = y; - } - - /** - * Set a box attribute, or defer it if the box is not yet created - * @param {Object} key - * @param {Object} value - */ - function boxAttr(key, value) { - if (box) { - box.attr(key, value); - } else { - deferredAttr[key] = value; - } - } - - function getSizeAfterAdd() { - wrapper.attr({ - text: str, // alignment is available now - x: x, - y: y - }); - - if (defined(anchorX)) { - wrapper.attr({ - anchorX: anchorX, - anchorY: anchorY - }); - } - } - - /** - * After the text element is added, get the desired size of the border box - * and add it before the text in the DOM. - */ - addEvent(wrapper, 'add', getSizeAfterAdd); - - /* - * Add specific attribute setters. - */ - - // only change local variables - attrSetters.width = function (value) { - width = value; - return false; - }; - attrSetters.height = function (value) { - height = value; - return false; - }; - attrSetters.padding = function (value) { - if (defined(value) && value !== padding) { - padding = value; - updateTextPadding(); - } - - return false; - }; - - // change local variable and set attribue as well - attrSetters.align = function (value) { - alignFactor = { left: 0, center: 0.5, right: 1 }[value]; - return false; // prevent setting text-anchor on the group - }; - - // apply these to the box and the text alike - attrSetters.text = function (value, key) { - text.attr(key, value); - updateBoxSize(); - updateTextPadding(); - return false; - }; - - // apply these to the box but not to the text - attrSetters[STROKE_WIDTH] = function (value, key) { - crispAdjust = value % 2 / 2; - boxAttr(key, value); - return false; - }; - attrSetters.stroke = attrSetters.fill = attrSetters.r = function (value, key) { - boxAttr(key, value); - return false; - }; - attrSetters.anchorX = function (value, key) { - anchorX = value; - boxAttr(key, value + crispAdjust - wrapperX); - return false; - }; - attrSetters.anchorY = function (value, key) { - anchorY = value; - boxAttr(key, value - wrapperY); - return false; - }; - - // rename attributes - attrSetters.x = function (value) { - wrapper.x = value; // for animation getter - value -= alignFactor * ((width || bBox.width) + padding); - wrapperX = mathRound(value); - - wrapper.attr('translateX', wrapperX); - return false; - }; - attrSetters.y = function (value) { - wrapperY = wrapper.y = mathRound(value); - wrapper.attr('translateY', value); - return false; - }; - - // Redirect certain methods to either the box or the text - var baseCss = wrapper.css; - return extend(wrapper, { - /** - * Pick up some properties and apply them to the text instead of the wrapper - */ - css: function (styles) { - if (styles) { - var textStyles = {}; - styles = merge({}, styles); // create a copy to avoid altering the original object (#537) - each(['fontSize', 'fontWeight', 'fontFamily', 'color', 'lineHeight', 'width'], function (prop) { - if (styles[prop] !== UNDEFINED) { - textStyles[prop] = styles[prop]; - delete styles[prop]; - } - }); - text.css(textStyles); - } - return baseCss.call(wrapper, styles); - }, - /** - * Return the bounding box of the box, not the group - */ - getBBox: function () { - return box.getBBox(); - }, - /** - * Apply the shadow to the box - */ - shadow: function (b) { - box.shadow(b); - return wrapper; - }, - /** - * Destroy and release memory. - */ - destroy: function () { - removeEvent(wrapper, 'add', getSizeAfterAdd); - - // Added by button implementation - removeEvent(wrapper.element, 'mouseenter'); - removeEvent(wrapper.element, 'mouseleave'); - - if (text) { - // Destroy the text element - text = text.destroy(); - } - // Call base implementation to destroy the rest - SVGElement.prototype.destroy.call(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. * - * * - *****************************************************************************/ - -/** - * @constructor - */ -var VMLRenderer; -if (!hasSVG && !useCanVG) { - -/** - * The VML element wrapper. - */ -var VMLElement = { - - /** - * 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 wrapper = this, - 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:1px;height:1px;'); - } - 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); - wrapper.element = createElement(markup); - } - - wrapper.renderer = renderer; - wrapper.attrSetters = {}; - }, - - /** - * 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.deferUpdateTransform) { - wrapper.updateTransform(); - } - - // fire an event for internal hooks - fireEvent(wrapper, 'add'); - - return wrapper; - }, - - /** - * In IE8 documentMode 8, we need to recursively set the visibility down in the DOM - * tree for nested groups. Related to #61, #586. - */ - toggleChildren: function (element, visibility) { - var childNodes = element.childNodes, - i = childNodes.length; - - while (i--) { - - // apply the visibility - css(childNodes[i], { visibility: visibility }); - - // we have a nested group, apply it to its children again - if (childNodes[i].nodeName === 'DIV') { - this.toggleChildren(childNodes[i], visibility); - } - } - }, - - /** - * VML always uses htmlUpdateTransform - */ - updateTransform: SVGElement.prototype.htmlUpdateTransform, - - /** - * Get or set attributes - */ - attr: function (hash, val) { - var wrapper = this, - key, - value, - i, - result, - element = wrapper.element || {}, - elemStyle = element.style, - nodeName = element.nodeName, - renderer = wrapper.renderer, - symbolName = wrapper.symbolName, - hasSetSymbolSize, - shadows = wrapper.shadows, - skipAttr, - attrSetters = wrapper.attrSetters, - ret = wrapper; - - // 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 = wrapper.strokeweight; - } else { - ret = wrapper[key]; - } - - // setter - } else { - for (key in hash) { - value = hash[key]; - skipAttr = false; - - // check for a specific attribute setter - result = attrSetters[key] && attrSetters[key](value, key); - - if (result !== false && value !== null) { // #620 - - if (result !== UNDEFINED) { - value = result; // the attribute setter has returned a new value to set - } - - - // prepare paths - // symbols - if (symbolName && /^(x|y|r|start|end|width|height|innerR|anchorX|anchorY)/.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) { - wrapper.symbolAttr(hash); - - hasSetSymbolSize = true; - } - skipAttr = true; - - } else if (key === 'd') { - value = value || []; - wrapper.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 = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value; - } - } - skipAttr = true; - - // directly mapped to css - } else if (key === 'zIndex' || key === 'visibility') { - - // workaround for #61 and #586 - if (docMode8 && key === 'visibility' && nodeName === 'DIV') { - element.gVis = value; - wrapper.toggleChildren(element, value); - if (value === VISIBLE) { // #74 - value = null; - } - } - - if (value) { - elemStyle[key] = value; - } - - - - skipAttr = true; - - // width and height - } else if (key === 'width' || key === 'height') { - - value = mathMax(0, value); // don't set width or height below zero (#311) - - this[key] = value; // used in getter - - // clipping rectangle special - if (wrapper.updateClipping) { - wrapper[key] = value; - wrapper.updateClipping(); - } else { - // normal - elemStyle[key] = value; - } - - skipAttr = true; - - // x and y - } else if (key === 'x' || key === 'y') { - - wrapper[key] = value; // used in getter - 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'; - wrapper[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'; - wrapper.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'; - } - - // rotation on VML elements - } else if (nodeName === 'shape' && key === 'rotation') { - wrapper[key] = value; - - // translation for animation - } else if (key === 'translateX' || key === 'translateY' || key === 'rotation') { - wrapper[key] = value; - wrapper.updateTransform(); - - skipAttr = true; - - // text for rotated and non-rotated elements - } else if (key === 'text') { - 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, - element = wrapper.element, - parentNode = element.parentNode; - - clipMembers.push(wrapper); - wrapper.destroyClip = function () { - erase(clipMembers, wrapper); - }; - - // Issue #863 workaround - related to #140, #61, #74 - if (parentNode && parentNode.className === 'highcharts-tracker' && !docMode8) { - css(element, { visibility: HIDDEN }); - } - - return wrapper.css(clipRect.getCSS(wrapper)); - }, - - /** - * Set styles for the element - * @param {Object} styles - */ - css: SVGElement.prototype.htmlCss, - - /** - * 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); - } - }, - - /** - * 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; - }, - - /** - * In stacked columns, cut off the shadows so that they don't overlap - */ - cutOffPath: function (path, length) { - - var len; - - path = path.split(/[ ,]/); - len = path.length; - - if (len === 9 || len === 11) { - path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length; - } - return path.join(' '); - }, - - /** - * Apply a drop shadow by copying elements and giving them different strokes - * @param {Boolean} apply - */ - shadow: function (apply, group, cutOff) { - var shadows = [], - i, - element = this.element, - renderer = this.renderer, - shadow, - elemStyle = element.style, - markup, - path = element.path, - strokeWidth, - modifiedPath; - - // some times empty paths are not strings - if (path && typeof path.value !== 'string') { - path = 'x'; - } - modifiedPath = path; - - if (apply) { - for (i = 1; i <= 3; i++) { - - strokeWidth = 7 - 2 * i; - - // Cut off shadows for stacked column items - if (cutOff) { - modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5); - } - - markup = ['']; - - shadow = createElement(renderer.prepVML(markup), - null, { - left: pInt(elemStyle.left) + 1, - top: pInt(elemStyle.top) + 1 - } - ); - if (cutOff) { - shadow.cutOff = strokeWidth + 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; - - } -}; -VMLElement = extendClass(SVGElement, VMLElement); - -/** - * The VML renderer - */ -var VMLRendererExtension = { // 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, - box; - - renderer.alignedObjects = []; - - boxWrapper = renderer.createElement(DIV); - box = boxWrapper.element; - box.style.position = RELATIVE; // for freeform drawing using renderer directly - container.appendChild(boxWrapper.element); - - - // generate the containing box - renderer.box = box; - 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; } '; - - } - }, - - - /** - * Detect whether the renderer is hidden. This happens when one of the parent elements - * has display: none - */ - isHidden: function () { - return !this.box.offsetWidth; - }, - - /** - * 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 (wrapper) { - var inverted = wrapper.inverted, - rect = this, - 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 && wrapper.element.nodeName !== 'IMG') { - 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)); - }); - } - }); - - }, - - - /** - * 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, - fillType, - ret = NONE; - - // Check for linear or radial gradient - if (color && color.linearGradient) { - fillType = 'gradient'; - } else if (color && color.radialGradient) { - fillType = 'pattern'; - } - - - if (fillType) { - - var stopColor, - stopOpacity, - gradient = color.linearGradient || color.radialGradient, - x1, - y1, - x2, - y2, - angle, - opacity1, - opacity2, - color1, - color2, - fillAttr = '', - stops = color.stops, - firstStop, - lastStop, - colors = []; - - // Extend from 0 to 1 - firstStop = stops[0]; - lastStop = stops[stops.length - 1]; - if (firstStop[0] > 0) { - stops.unshift([ - 0, - firstStop[1] - ]); - } - if (lastStop[0] < 1) { - stops.push([ - 1, - lastStop[1] - ]); - } - - // Compute the stops - each(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; - } - - // Build the color attribute - colors.push((stop[0] * 100) + '% ' + stopColor); - - // Only start and end opacities are allowed, so we use the first and the last - if (!i) { - opacity1 = stopOpacity; - color2 = stopColor; - } else { - opacity2 = stopOpacity; - color1 = stopColor; - } - }); - - // Handle linear gradient angle - if (fillType === 'gradient') { - x1 = gradient.x1 || gradient[0] || 0; - y1 = gradient.y1 || gradient[1] || 0; - x2 = gradient.x2 || gradient[2] || 0; - y2 = gradient.y2 || gradient[3] || 0; - angle = 90 - math.atan( - (y2 - y1) / // y vector - (x2 - x1) // x vector - ) * 180 / mathPI; - - // Radial (circular) gradient - } else { - // pie: http://jsfiddle.net/highcharts/66g8H/ - // reference: http://jsfiddle.net/highcharts/etznJ/ - // http://jsfiddle.net/highcharts/XRbCc/ - // http://jsfiddle.net/highcharts/F3fwR/ - // TODO: - // - correct for radialRefeence - // - check whether gradient stops are supported - // - add global option for gradient image (must relate to version) - var r = gradient.r, - size = r * 2, - cx = gradient.cx, - cy = gradient.cy; - //radialReference = elem.radialReference; - - //if (radialReference) { - // Try setting pixel size, or other way to adjust the gradient size to the bounding box - //} - fillAttr = 'src="http://code.highcharts.com/gfx/radial-gradient.png" ' + - 'size="' + size + ',' + size + '" ' + - 'origin="0.5,0.5" ' + - 'position="' + cx + ',' + cy + '" ' + - 'color2="' + color2 + '" '; - - // The fill element's color attribute is broken in IE8 standards mode, so we - // need to set the parent shape's fillcolor attribute instead. - ret = color1; - } - - - - // Apply the gradient to fills only. - if (prop === 'fill') { - - // when colors attribute is used, the meanings of opacity and o:opacity2 - // are reversed. - markup = ['']; - createElement(this.prepVML(markup), null, null, elem); - - // Gradients are not supported for VML stroke, return the first color. #722. - } else { - ret = stopColor; - } - - - // 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); - - ret = colorObject.get('rgb'); - - - } else { - var strokeNodes = elem.getElementsByTagName(prop); - if (strokeNodes.length) { - strokeNodes[0].opacity = 1; - } - ret = color; - } - - return ret; - }, - - /** - * 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; - 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) - 1, - top: pInt(parentStyle.height) - 1, - rotation: -90 - }); - }, - - /** - * Symbol definitions that override the parent SVG renderer's symbols - * - */ - symbols: { - // VML specific arc function - arc: function (x, y, w, h, options) { - var start = options.start, - end = options.end, - radius = options.r || w || h, - cosStart = mathCos(start), - sinStart = mathSin(start), - cosEnd = mathCos(end), - sinEnd = mathSin(end), - innerRadius = options.innerR, - circleCorrection = 0.08 / radius, // #760 - innerCorrection = (innerRadius && 0.1 / innerRadius) || 0, - ret; - - if (end - start === 0) { // no angle, don't show it. - return ['x']; - - } 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); - } - - ret = [ - '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 - ]; - - if (options.open) { - ret.push( - M, - x - innerRadius, - y - innerRadius - ); - } - - ret.push( - '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 - ); - - return ret; - - }, - // Add circle symbol path. This performs significantly faster than v:oval. - circle: function (x, y, w, h) { - - return [ - 'wa', // clockwisearcto - x, // left - y, // top - x + w, // right - y + h, // bottom - x + w, // start x - y + h / 2, // start y - x + w, // end x - y + h / 2, // 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, width, height, options) { - - var right = left + width, - bottom = top + height, - ret, - r; - - // No radius, return the more lightweight square - if (!defined(options) || !options.r) { - ret = SVGRenderer.prototype.symbols.square.apply(0, arguments); - - // Has radius add arcs for the corners - } else { - - r = mathMin(options.r, width, height); - ret = [ - 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' - ]; - } - return ret; - } - } -}; -VMLRenderer = function () { - this.init.apply(this, arguments); -}; -VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension); - - // general renderer - Renderer = VMLRenderer; -} - -/* **************************************************************************** - * * - * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE * - * * - *****************************************************************************/ -/* **************************************************************************** - * * - * START OF ANDROID < 3 SPECIFIC CODE. THIS CAN BE REMOVED IF YOU'RE NOT * - * TARGETING THAT SYSTEM. * - * * - *****************************************************************************/ -var CanVGRenderer, - CanVGController; - -if (useCanVG) { - /** - * The CanVGRenderer is empty from start to keep the source footprint small. - * When requested, the CanVGController downloads the rest of the source packaged - * together with the canvg library. - */ - CanVGRenderer = function () { - // Empty constructor - }; - - /** - * Start with an empty symbols object. This is needed when exporting is used (exporting.src.js will add a few symbols), but - * the implementation from SvgRenderer will not be merged in until first render. - */ - CanVGRenderer.prototype.symbols = {}; - - /** - * Handles on demand download of canvg rendering support. - */ - CanVGController = (function () { - // List of renderering calls - var deferredRenderCalls = []; - - /** - * When downloaded, we are ready to draw deferred charts. - */ - function drawDeferred() { - var callLength = deferredRenderCalls.length, - callIndex; - - // Draw all pending render calls - for (callIndex = 0; callIndex < callLength; callIndex++) { - deferredRenderCalls[callIndex](); - } - // Clear the list - deferredRenderCalls = []; - } - - return { - push: function (func, scriptLocation) { - // Only get the script once - if (deferredRenderCalls.length === 0) { - getScript(scriptLocation, drawDeferred); - } - // Register render call - deferredRenderCalls.push(func); - } - }; - }()); -} // end CanVGRenderer - -/* **************************************************************************** - * * - * END OF ANDROID < 3 SPECIFIC CODE * - * * - *****************************************************************************/ - -/** - * General renderer - */ -Renderer = VMLRenderer || CanVGRenderer || SVGRenderer; -/** - * The Tick class - */ -function Tick(axis, pos, type) { - this.axis = axis; - this.pos = pos; - this.type = type || ''; - this.isNew = true; - - if (!type) { - this.addLabel(); - } -} - -Tick.prototype = { - /** - * Write the tick label - */ - addLabel: function () { - var tick = this, - axis = tick.axis, - options = axis.options, - chart = axis.chart, - horiz = axis.horiz, - categories = axis.categories, - pos = tick.pos, - labelOptions = options.labels, - str, - tickPositions = axis.tickPositions, - width = (categories && horiz && categories.length && - !labelOptions.step && !labelOptions.staggerLines && - !labelOptions.rotation && - chart.plotWidth / tickPositions.length) || - (!horiz && chart.plotWidth / 2), - isFirst = pos === tickPositions[0], - isLast = pos === tickPositions[tickPositions.length - 1], - css, - attr, - value = categories && defined(categories[pos]) ? categories[pos] : pos, - label = tick.label, - tickPositionInfo = tickPositions.info, - dateTimeLabelFormat; - - // Set the datetime label format. If a higher rank is set for this position, use that. If not, - // use the general format. - if (axis.isDatetimeAxis && tickPositionInfo) { - dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName]; - } - - // set properties for access in render method - tick.isFirst = isFirst; - tick.isLast = isLast; - - // get the string - str = axis.labelFormatter.call({ - axis: axis, - chart: chart, - isFirst: isFirst, - isLast: isLast, - dateTimeLabelFormat: dateTimeLabelFormat, - value: axis.isLog ? correctFloat(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 (!defined(label)) { - attr = { - align: labelOptions.align - }; - if (isNumber(labelOptions.rotation)) { - attr.rotation = labelOptions.rotation; - } - tick.label = - defined(str) && labelOptions.enabled ? - chart.renderer.text( - str, - 0, - 0, - labelOptions.useHTML - ) - .attr(attr) - // without position absolute, IE export sometimes is wrong - .css(css) - .add(axis.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, - axis = this.axis; - return label ? - ((this.labelBBox = label.getBBox(true)))[axis.horiz ? 'height' : 'width'] : - 0; - }, - - /** - * Find how far the labels extend to the right and left of the tick's x position. Used for anti-collision - * detection with overflow logic. - */ - getLabelSides: function () { - var bBox = this.labelBBox, // assume getLabelSize has run at this point - axis = this.axis, - options = axis.options, - labelOptions = options.labels, - width = bBox.width, - leftSide = width * { left: 0, center: 0.5, right: 1 }[labelOptions.align] - labelOptions.x; - - return [-leftSide, width - leftSide]; - }, - - /** - * Handle the label overflow by adjusting the labels to the left and right edge, or - * hide them if they collide into the neighbour label. - */ - handleOverflow: function (index, xy) { - var show = true, - axis = this.axis, - chart = axis.chart, - isFirst = this.isFirst, - isLast = this.isLast, - x = xy.x, - reversed = axis.reversed, - tickPositions = axis.tickPositions; - - if (isFirst || isLast) { - - var sides = this.getLabelSides(), - leftSide = sides[0], - rightSide = sides[1], - plotLeft = chart.plotLeft, - plotRight = plotLeft + axis.len, - neighbour = axis.ticks[tickPositions[index + (isFirst ? 1 : -1)]], - neighbourEdge = neighbour && neighbour.label.xy.x + neighbour.getLabelSides()[isFirst ? 0 : 1]; - - if ((isFirst && !reversed) || (isLast && reversed)) { - // Is the label spilling out to the left of the plot area? - if (x + leftSide < plotLeft) { - - // Align it to plot left - x = plotLeft - leftSide; - - // Hide it if it now overlaps the neighbour label - if (neighbour && x + rightSide > neighbourEdge) { - show = false; - } - } - - } else { - // Is the label spilling out to the right of the plot area? - if (x + rightSide > plotRight) { - - // Align it to plot right - x = plotRight - rightSide; - - // Hide it if it now overlaps the neighbour label - if (neighbour && x + leftSide < neighbourEdge) { - show = false; - } - - } - } - - // Set the modified x position of the label - xy.x = x; - } - return show; - }, - - /** - * Get the x and y position for ticks and labels - */ - getPosition: function (horiz, pos, tickmarkOffset, old) { - var axis = this.axis, - chart = axis.chart, - cHeight = (old && chart.oldChartHeight) || chart.chartHeight; - - return { - x: horiz ? - axis.translate(pos + tickmarkOffset, null, null, old) + axis.transB : - axis.left + axis.offset + (axis.opposite ? ((old && chart.oldChartWidth) || chart.chartWidth) - axis.right - axis.left : 0), - - y: horiz ? - cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) : - cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB - }; - - }, - - /** - * Get the x, y position of the tick label - */ - getLabelPosition: function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) { - var axis = this.axis, - transA = axis.transA, - reversed = axis.reversed, - staggerLines = axis.staggerLines; - - 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; - } - - return { - x: x, - y: y - }; - }, - - /** - * Extendible method to return the path of the marker - */ - getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) { - return renderer.crispLine([ - M, - x, - y, - L, - x + (horiz ? 0 : -tickLength), - y + (horiz ? tickLength : 0) - ], tickWidth); - }, - - /** - * 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, - axis = tick.axis, - options = axis.options, - chart = axis.chart, - renderer = chart.renderer, - horiz = axis.horiz, - type = tick.type, - label = tick.label, - pos = tick.pos, - labelOptions = options.labels, - gridLine = tick.gridLine, - gridPrefix = type ? type + 'Grid' : 'grid', - tickPrefix = type ? type + 'Tick' : 'tick', - gridLineWidth = options[gridPrefix + 'LineWidth'], - gridLineColor = options[gridPrefix + 'LineColor'], - dashStyle = options[gridPrefix + 'LineDashStyle'], - tickLength = options[tickPrefix + 'Length'], - tickWidth = options[tickPrefix + 'Width'] || 0, - tickColor = options[tickPrefix + 'Color'], - tickPosition = options[tickPrefix + 'Position'], - gridLinePath, - mark = tick.mark, - markPath, - step = labelOptions.step, - attribs, - show = true, - tickmarkOffset = (options.categories && options.tickmarkPlacement === 'between') ? 0.5 : 0, - xy = tick.getPosition(horiz, pos, tickmarkOffset, old), - x = xy.x, - y = xy.y, - staggerLines = axis.staggerLines; - - // create the grid line - if (gridLineWidth) { - gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth, old); - - if (gridLine === UNDEFINED) { - attribs = { - stroke: gridLineColor, - 'stroke-width': gridLineWidth - }; - if (dashStyle) { - attribs.dashstyle = dashStyle; - } - if (!type) { - attribs.zIndex = 1; - } - tick.gridLine = gridLine = - gridLineWidth ? - renderer.path(gridLinePath) - .attr(attribs).add(axis.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[tick.isNew ? 'attr' : 'animate']({ - d: gridLinePath - }); - } - } - - // create the tick mark - if (tickWidth) { - - // negate the length - if (tickPosition === 'inside') { - tickLength = -tickLength; - } - if (axis.opposite) { - tickLength = -tickLength; - } - - markPath = tick.getMarkPath(x, y, tickLength, tickWidth, horiz, renderer); - - if (mark) { // updating - mark.animate({ - d: markPath - }); - } else { // first time - tick.mark = renderer.path( - markPath - ).attr({ - stroke: tickColor, - 'stroke-width': tickWidth - }).add(axis.axisGroup); - } - } - - // the label is created on init - now move it into place - if (label && !isNaN(x)) { - label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step); - - // apply show first and show last - if ((tick.isFirst && !pick(options.showFirstLabel, 1)) || - (tick.isLast && !pick(options.showLastLabel, 1))) { - show = false; - - // Handle label overflow and show or hide accordingly - } else if (!staggerLines && horiz && labelOptions.overflow === 'justify' && !tick.handleOverflow(index, xy)) { - show = false; - } - - // apply step - if (step && index % step) { - // show those indices dividable by step - show = false; - } - - // Set the new position, and show or hide - if (show) { - label[tick.isNew ? 'attr' : 'animate'](xy); - label.show(); - tick.isNew = false; - } else { - label.hide(); - } - } - }, - - /** - * Destructor for the tick prototype - */ - destroy: function () { - destroyObjectProperties(this, this.axis); - } -}; - -/** - * The object wrapper for plot lines and plot bands - * @param {Object} options - */ -function PlotLineOrBand(axis, options) { - this.axis = axis; - - if (options) { - this.options = options; - this.id = options.id; - } - - //plotLine.render() - return this; -} - -PlotLineOrBand.prototype = { - - /** - * Render the plot line or plot band. If it is already existing, - * move it. - */ - render: function () { - var plotLine = this, - axis = plotLine.axis, - horiz = axis.horiz, - halfPointRange = (axis.pointRange || 0) / 2, - options = plotLine.options, - optionsLabel = options.label, - label = plotLine.label, - width = options.width, - to = options.to, - from = options.from, - isBand = defined(from) && defined(to), - value = options.value, - dashStyle = options.dashStyle, - svgElem = plotLine.svgElem, - path = [], - addEvent, - eventType, - xs, - ys, - x, - y, - color = options.color, - zIndex = options.zIndex, - events = options.events, - attribs, - renderer = axis.chart.renderer; - - // logarithmic conversion - if (axis.isLog) { - from = log2lin(from); - to = log2lin(to); - value = log2lin(value); - } - - // plot line - if (width) { - path = axis.getPlotLinePath(value, width); - attribs = { - stroke: color, - 'stroke-width': width - }; - if (dashStyle) { - attribs.dashstyle = dashStyle; - } - } else if (isBand) { // plot band - - // keep within plot area - from = mathMax(from, axis.min - halfPointRange); - to = mathMin(to, axis.max + halfPointRange); - - path = axis.getPlotBandPath(from, to, options); - attribs = { - fill: color - }; - if (options.borderWidth) { - attribs.stroke = options.borderColor; - attribs['stroke-width'] = options.borderWidth; - } - } 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 && axis.width > 0 && axis.height > 0) { - // apply defaults - optionsLabel = merge({ - align: horiz && isBand && 'center', - x: horiz ? !isBand && 4 : 10, - verticalAlign : !horiz && isBand && 'middle', - y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4, - rotation: horiz && !isBand && 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 = arrayMin(xs); - y = arrayMin(ys); - - label.align(optionsLabel, false, { - x: x, - y: y, - width: arrayMax(xs) - x, - height: arrayMax(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 plotLine = this, - axis = plotLine.axis; - - // remove it from the lookup - erase(axis.plotLinesAndBands, plotLine); - - destroyObjectProperties(plotLine, this.axis); - } -}; -/** - * The class for stack items - */ -function StackItem(axis, options, isNegative, x, stackOption) { - var inverted = axis.chart.inverted; - - this.axis = axis; - - // Tells if the stack is negative - this.isNegative = isNegative; - - // Save the options to be able to style the label - this.options = options; - - // Save the x value to be able to position the label later - this.x = x; - - // Save the stack option on the series configuration object - this.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. - this.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) - }; - - this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center'); -} - -StackItem.prototype = { - destroy: function () { - destroyObjectProperties(this, this.axis); - }, - - /** - * 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 str = this.options.formatter.call(this); // 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 (this.label) { - this.label.attr({text: str, visibility: HIDDEN}); - // Create new label - } else { - this.label = - this.axis.chart.renderer.text(str, 0, 0) // dummy positions, actual position updated with setOffset method in columnseries - .css(this.options.style) // apply style - .attr({align: this.textAlign, // fix the text-anchor - rotation: this.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, - axis = stackItem.axis, - chart = axis.chart, - inverted = chart.inverted, - neg = this.isNegative, // special treatment is needed for negative stacks - y = axis.translate(this.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates - yZero = axis.translate(0), // stack origin - h = mathAbs(y - yZero), // stack height - x = chart.xAxis[0].translate(this.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 (this.label) { - this.label - .align(this.alignOptions, null, stackBox) // align the label to the box - .attr({visibility: VISIBLE}); // set visibility - } - - } -}; -/** - * Create a new axis object - * @param {Object} chart - * @param {Object} options - */ -function Axis() { - this.init.apply(this, arguments); -} - -Axis.prototype = { - - /** - * Default options for the X axis - the Y axis has extended defaults - */ - defaultOptions: { - // allowDecimals: null, - // alternateGridColor: null, - // categories: [], - dateTimeLabelFormats: { - millisecond: '%H:%M:%S.%L', - second: '%H:%M:%S', - minute: '%H:%M', - hour: '%H:%M', - day: '%e. %b', - week: '%e. %b', - month: '%b \'%y', - year: '%Y' - }, - endOnTick: false, - gridLineColor: '#C0C0C0', - // gridLineDashStyle: 'solid', - // gridLineWidth: 0, - // reversed: false, - - labels: defaultLabelOptions, - // { step: null }, - lineColor: '#C0D0E0', - lineWidth: 1, - //linkedTo: null, - //max: undefined, - //min: undefined, - minPadding: 0.01, - maxPadding: 0.01, - //minRange: null, - minorGridLineColor: '#E0E0E0', - // minorGridLineDashStyle: null, - minorGridLineWidth: 1, - minorTickColor: '#A0A0A0', - //minorTickInterval: null, - minorTickLength: 2, - minorTickPosition: 'outside', // inside or outside - //minorTickWidth: 0, - //opposite: false, - //offset: 0, - //plotBands: [{ - // events: {}, - // zIndex: 1, - // labels: { align, x, verticalAlign, y, style, rotation, textAlign } - //}], - //plotLines: [{ - // events: {} - // dashStyle: {} - // zIndex: - // labels: { align, x, verticalAlign, y, style, rotation, textAlign } - //}], - //reversed: false, - // showFirstLabel: true, - // showLastLabel: true, - startOfWeek: 1, - startOnTick: false, - tickColor: '#C0D0E0', - //tickInterval: null, - tickLength: 5, - tickmarkPlacement: 'between', // on or between - tickPixelInterval: 100, - tickPosition: 'outside', - tickWidth: 1, - title: { - //text: null, - align: 'middle', // low, middle or high - //margin: 0 for horizontal, 10 for vertical axes, - //rotation: 0, - //side: 'outside', - style: { - color: '#6D869F', - //font: defaultFont.replace('normal', 'bold') - fontWeight: 'bold' - } - //x: 0, - //y: 0 - }, - type: 'linear' // linear, logarithmic or datetime - }, - - /** - * This options set extends the defaultOptions for Y axes - */ - defaultYAxisOptions: { - endOnTick: true, - gridLineWidth: 1, - tickPixelInterval: 72, - showLastLabel: true, - labels: { - align: 'right', - x: -8, - y: 3 - }, - lineWidth: 0, - maxPadding: 0.05, - minPadding: 0.05, - startOnTick: true, - tickWidth: 0, - title: { - rotation: 270, - text: 'Y-values' - }, - stackLabels: { - enabled: false, - //align: dynamic, - //y: dynamic, - //x: dynamic, - //verticalAlign: dynamic, - //textAlign: dynamic, - //rotation: 0, - formatter: function () { - return this.total; - }, - style: defaultLabelOptions.style - } - }, - - /** - * These options extend the defaultOptions for left axes - */ - defaultLeftAxisOptions: { - labels: { - align: 'right', - x: -8, - y: null - }, - title: { - rotation: 270 - } - }, - - /** - * These options extend the defaultOptions for right axes - */ - defaultRightAxisOptions: { - labels: { - align: 'left', - x: 8, - y: null - }, - title: { - rotation: 90 - } - }, - - /** - * These options extend the defaultOptions for bottom axes - */ - defaultBottomAxisOptions: { - labels: { - align: 'center', - x: 0, - y: 14 - // overflow: undefined, - // staggerLines: null - }, - title: { - rotation: 0 - } - }, - /** - * These options extend the defaultOptions for left axes - */ - defaultTopAxisOptions: { - labels: { - align: 'center', - x: 0, - y: -5 - // overflow: undefined - // staggerLines: null - }, - title: { - rotation: 0 - } - }, - - /** - * Initialize the axis - */ - init: function (chart, userOptions) { - - - var isXAxis = userOptions.isX, - axis = this; - - // Flag, is the axis horizontal - axis.horiz = chart.inverted ? !isXAxis : isXAxis; - - // Flag, isXAxis - axis.isXAxis = isXAxis; - axis.xOrY = isXAxis ? 'x' : 'y'; - - - axis.opposite = userOptions.opposite; // needed in setOptions - axis.side = axis.horiz ? - (axis.opposite ? 0 : 2) : // top : bottom - (axis.opposite ? 1 : 3); // right : left - - axis.setOptions(userOptions); - - - var options = this.options, - type = options.type, - isDatetimeAxis = type === 'datetime'; - - axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format - - - // Flag, stagger lines or not - axis.staggerLines = axis.horiz && options.labels.staggerLines; - axis.userOptions = userOptions; - - //axis.axisTitleMargin = UNDEFINED,// = options.title.margin, - axis.minPixelPadding = 0; - //axis.ignoreMinPadding = UNDEFINED; // can be set to true by a column or bar series - //axis.ignoreMaxPadding = UNDEFINED; - - axis.chart = chart; - axis.reversed = options.reversed; - - // Initial categories - axis.categories = options.categories; - - // Elements - //axis.axisGroup = UNDEFINED; - //axis.gridGroup = UNDEFINED; - //axis.axisTitle = UNDEFINED; - //axis.axisLine = UNDEFINED; - - // Flag if type === logarithmic - axis.isLog = type === 'logarithmic'; - - // Flag, if axis is linked to another axis - axis.isLinked = defined(options.linkedTo); - // Linked axis. - //axis.linkedParent = UNDEFINED; - - // Flag if type === datetime - axis.isDatetimeAxis = isDatetimeAxis; - - // Flag if percentage mode - //axis.usePercentage = UNDEFINED; - - - // Tick positions - //axis.tickPositions = UNDEFINED; // array containing predefined positions - // Tick intervals - //axis.tickInterval = UNDEFINED; - //axis.minorTickInterval = UNDEFINED; - - // Major ticks - axis.ticks = {}; - // Minor ticks - axis.minorTicks = {}; - //axis.tickAmount = UNDEFINED; - - // List of plotLines/Bands - axis.plotLinesAndBands = []; - - // Alternate bands - axis.alternateBands = {}; - - // Axis metrics - //axis.left = UNDEFINED; - //axis.top = UNDEFINED; - //axis.width = UNDEFINED; - //axis.height = UNDEFINED; - //axis.bottom = UNDEFINED; - //axis.right = UNDEFINED; - //axis.transA = UNDEFINED; - //axis.transB = UNDEFINED; - //axis.oldTransA = UNDEFINED; - axis.len = 0; - //axis.oldMin = UNDEFINED; - //axis.oldMax = UNDEFINED; - //axis.oldUserMin = UNDEFINED; - //axis.oldUserMax = UNDEFINED; - //axis.oldAxisLength = UNDEFINED; - axis.minRange = axis.userMinRange = options.minRange || options.maxZoom; - axis.range = options.range; - axis.offset = options.offset || 0; - - - // Dictionary for stacks - axis.stacks = {}; - - // Min and max in the data - //axis.dataMin = UNDEFINED, - //axis.dataMax = UNDEFINED, - - // The axis range - axis.max = null; - axis.min = null; - - // User set min and max - //axis.userMin = UNDEFINED, - //axis.userMax = UNDEFINED, - - // Run Axis - - var eventType, - events = axis.options.events; - - // Register - chart.axes.push(axis); - chart[isXAxis ? 'xAxis' : 'yAxis'].push(axis); - - axis.series = []; // populated by Series - - // inverted charts have reversed xAxes as default - if (chart.inverted && isXAxis && axis.reversed === UNDEFINED) { - axis.reversed = true; - } - - axis.removePlotBand = axis.removePlotBandOrLine; - axis.removePlotLine = axis.removePlotBandOrLine; - axis.addPlotBand = axis.addPlotBandOrLine; - axis.addPlotLine = axis.addPlotBandOrLine; - - - // register event listeners - for (eventType in events) { - addEvent(axis, eventType, events[eventType]); - } - - // extend logarithmic axis - if (axis.isLog) { - axis.val2lin = log2lin; - axis.lin2val = lin2log; - } - }, - - /** - * Merge and set options - */ - setOptions: function (userOptions) { - this.options = merge( - this.defaultOptions, - this.isXAxis ? {} : this.defaultYAxisOptions, - [this.defaultTopAxisOptions, this.defaultRightAxisOptions, - this.defaultBottomAxisOptions, this.defaultLeftAxisOptions][this.side], - userOptions - ); - }, - - - /** - * The default label formatter. The context is a special config object for the label. - */ - defaultLabelFormatter: function () { - var axis = this.axis, - value = this.value, - categories = axis.categories, - tickInterval = axis.tickInterval, - dateTimeLabelFormat = this.dateTimeLabelFormat, - ret; - - if (categories) { - ret = value; - - } else 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 (value >= 1000) { // add thousands separators - ret = numberFormat(value, 0); - - } else { // small numbers - ret = numberFormat(value, -1); - } - return ret; - }, - - /** - * Get the minimum and maximum for the series of each axis - */ - getSeriesExtremes: function () { - var axis = this, - chart = axis.chart, - stacks = axis.stacks, - posStack = [], - negStack = [], - i; - - // reset dataMin and dataMax in case we're redrawing - axis.dataMin = axis.dataMax = null; - - // loop through this axis' series - each(axis.series, function (series) { - - if (series.visible || !chart.options.chart.ignoreHiddenSeries) { - - var seriesOptions = series.options, - stacking, - posPointStack, - negPointStack, - stackKey, - stackOption, - negKey, - xData, - yData, - x, - y, - threshold = seriesOptions.threshold, - yDataLength, - activeYData = [], - activeCounter = 0; - - // Validate threshold in logarithmic axes - if (axis.isLog && threshold <= 0) { - threshold = seriesOptions.threshold = null; - } - - // Get dataMin and dataMax for X axes - if (axis.isXAxis) { - xData = series.xData; - if (xData.length) { - axis.dataMin = mathMin(pick(axis.dataMin, xData[0]), arrayMin(xData)); - axis.dataMax = mathMax(pick(axis.dataMax, xData[0]), arrayMax(xData)); - } - - // Get dataMin and dataMax for Y axes, as well as handle stacking and processed data - } else { - var isNegative, - pointStack, - key, - cropped = series.cropped, - xExtremes = series.xAxis.getExtremes(), - //findPointRange, - //pointRange, - j, - hasModifyValue = !!series.modifyValue; - - - // Handle stacking - stacking = seriesOptions.stacking; - axis.usePercentage = stacking === 'percent'; - - // create a stack for this particular series type - if (stacking) { - stackOption = seriesOptions.stack; - stackKey = series.type + pick(stackOption, ''); - negKey = '-' + stackKey; - series.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 (axis.usePercentage) { - axis.dataMin = 0; - axis.dataMax = 99; - } - - // processData can alter series.pointRange, so this goes after - //findPointRange = series.pointRange === null; - - xData = series.processedXData; - yData = series.processedYData; - yDataLength = yData.length; - - // loop over the non-null y values and read them into a local array - for (i = 0; i < yDataLength; i++) { - x = xData[i]; - y = yData[i]; - if (y !== null && y !== UNDEFINED) { - - // read stacked values into a stack based on the x value, - // the sign of y and the stack key - if (stacking) { - isNegative = y < threshold; - pointStack = isNegative ? negPointStack : posPointStack; - key = isNegative ? negKey : stackKey; - - y = pointStack[x] = - defined(pointStack[x]) ? - pointStack[x] + y : y; - - - // 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][x]) { - stacks[key][x] = new StackItem(axis, axis.options.stackLabels, isNegative, x, stackOption); - } - stacks[key][x].setTotal(y); - - - // general hook, used for Highstock compare values feature - } else if (hasModifyValue) { - y = series.modifyValue(y); - } - - // get the smallest distance between points - /*if (i) { - distance = mathAbs(xData[i] - xData[i - 1]); - pointRange = pointRange === UNDEFINED ? distance : mathMin(distance, pointRange); - }*/ - - // for points within the visible range, including the first point outside the - // visible range, consider y extremes - if (cropped || ((xData[i + 1] || x) >= xExtremes.min && (xData[i - 1] || x) <= xExtremes.max)) { - - j = y.length; - if (j) { // array, like ohlc or range data - while (j--) { - if (y[j] !== null) { - activeYData[activeCounter++] = y[j]; - } - } - } else { - activeYData[activeCounter++] = y; - } - } - } - } - - // record the least unit distance - /*if (findPointRange) { - series.pointRange = pointRange || 1; - } - series.closestPointRange = pointRange;*/ - - // Get the dataMin and dataMax so far. If percentage is used, the min and max are - // always 0 and 100. If the length of activeYData is 0, continue with null values. - if (!axis.usePercentage && activeYData.length) { - axis.dataMin = mathMin(pick(axis.dataMin, activeYData[0]), arrayMin(activeYData)); - axis.dataMax = mathMax(pick(axis.dataMax, activeYData[0]), arrayMax(activeYData)); - } - - // Adjust to threshold - if (defined(threshold)) { - if (axis.dataMin >= threshold) { - axis.dataMin = threshold; - axis.ignoreMinPadding = true; - } else if (axis.dataMax < threshold) { - axis.dataMax = threshold; - axis.ignoreMaxPadding = true; - } - } - } - } - }); - }, - - /** - * Translate from axis value to pixel position on the chart, or back - * - */ - translate: function (val, backwards, cvsCoord, old, handleLog) { - var axis = this, - axisLength = axis.len, - sign = 1, - cvsOffset = 0, - localA = old ? axis.oldTransA : axis.transA, - localMin = old ? axis.oldMin : axis.min, - returnValue, - postTranslate = axis.options.ordinal || (axis.isLog && handleLog); - - if (!localA) { - localA = axis.transA; - } - - if (cvsCoord) { - sign *= -1; // canvas coordinates inverts the value - cvsOffset = axisLength; - } - if (axis.reversed) { // reversed axis - sign *= -1; - cvsOffset -= sign * axisLength; - } - - if (backwards) { // reverse translation - if (axis.reversed) { - val = axisLength - val; - } - returnValue = val / localA + localMin; // from chart pixel to value - if (postTranslate) { // log and ordinal axes - returnValue = axis.lin2val(returnValue); - } - - } else { // normal translation, from axis value to pixel, relative to plot - if (postTranslate) { // log and ordinal axes - val = axis.val2lin(val); - } - - returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * axis.minPixelPadding); - } - - 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 axis = this, - chart = axis.chart, - axisLeft = axis.left, - axisTop = axis.top, - x1, - y1, - x2, - y2, - translatedValue = axis.translate(value, null, null, old), - cHeight = (old && chart.oldChartHeight) || chart.chartHeight, - cWidth = (old && chart.oldChartWidth) || chart.chartWidth, - skip, - transB = axis.transB; - - x1 = x2 = mathRound(translatedValue + transB); - y1 = y2 = mathRound(cHeight - translatedValue - transB); - - if (isNaN(translatedValue)) { // no min or max - skip = true; - - } else if (axis.horiz) { - y1 = axisTop; - y2 = cHeight - axis.bottom; - if (x1 < axisLeft || x1 > axisLeft + axis.width) { - skip = true; - } - } else { - x1 = axisLeft; - x2 = cWidth - axis.right; - - if (y1 < axisTop || y1 > axisTop + axis.height) { - skip = true; - } - } - return skip ? - null : - chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 0); - }, - - /** - * Create the path for a plot band - */ - getPlotBandPath: function (from, to) { - - var toPath = this.getPlotLinePath(to), - path = this.getPlotLinePath(from); - - if (path && toPath) { - path.push( - toPath[4], - toPath[5], - toPath[1], - toPath[2] - ); - } else { // outside the axis area - path = null; - } - - return path; - }, - - /** - * Set the tick positions of a linear axis to round values like whole tens or every five. - */ - getLinearTickPositions: function (tickInterval, min, max) { - var pos, - lastPos, - roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval), - roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval), - tickPositions = []; - - // Populate the intermediate values - pos = roundedMin; - while (pos <= roundedMax) { - - // Place the tick on the rounded value - tickPositions.push(pos); - - // Always add the raw tickInterval, not the corrected one. - pos = correctFloat(pos + tickInterval); - - // If the interval is not big enough in the current min - max range to actually increase - // the loop variable, we need to break out to prevent endless loop. Issue #619 - if (pos === lastPos) { - break; - } - - // Record the last value - lastPos = pos; - } - return tickPositions; - }, - - /** - * Set the tick positions of a logarithmic axis - */ - getLogTickPositions: function (interval, min, max, minor) { - var axis = this, - options = axis.options, - axisLength = axis.len; - - // Since we use this method for both major and minor ticks, - // use a local variable and return the result - var positions = []; - - // Reset - if (!minor) { - axis._minorAutoInterval = null; - } - - // First case: All ticks fall on whole logarithms: 1, 10, 100 etc. - if (interval >= 0.5) { - interval = mathRound(interval); - positions = axis.getLinearTickPositions(interval, min, max); - - // Second case: We need intermediary ticks. For example - // 1, 2, 4, 6, 8, 10, 20, 40 etc. - } else if (interval >= 0.08) { - var roundedMin = mathFloor(min), - intermediate, - i, - j, - len, - pos, - lastPos, - break2; - - if (interval > 0.3) { - intermediate = [1, 2, 4]; - } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc - intermediate = [1, 2, 4, 6, 8]; - } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc - intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9]; - } - - for (i = roundedMin; i < max + 1 && !break2; i++) { - len = intermediate.length; - for (j = 0; j < len && !break2; j++) { - pos = log2lin(lin2log(i) * intermediate[j]); - - if (pos > min) { - positions.push(lastPos); - } - - if (lastPos > max) { - break2 = true; - } - lastPos = pos; - } - } - - // Third case: We are so deep in between whole logarithmic values that - // we might as well handle the tick positions like a linear axis. For - // example 1.01, 1.02, 1.03, 1.04. - } else { - var realMin = lin2log(min), - realMax = lin2log(max), - tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'], - filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption, - tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1), - totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength; - - interval = pick( - filteredTickIntervalOption, - axis._minorAutoInterval, - (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1) - ); - - interval = normalizeTickInterval( - interval, - null, - math.pow(10, mathFloor(math.log(interval) / math.LN10)) - ); - - positions = map(axis.getLinearTickPositions( - interval, - realMin, - realMax - ), log2lin); - - if (!minor) { - axis._minorAutoInterval = interval / 5; - } - } - - // Set the axis-level tickInterval variable - if (!minor) { - axis.tickInterval = interval; - } - return positions; - }, - - /** - * Return the minor tick positions. For logarithmic axes, reuse the same logic - * as for major ticks. - */ - getMinorTickPositions: function () { - var axis = this, - tickPositions = axis.tickPositions, - minorTickInterval = axis.minorTickInterval; - - var minorTickPositions = [], - pos, - i, - len; - - if (axis.isLog) { - len = tickPositions.length; - for (i = 1; i < len; i++) { - minorTickPositions = minorTickPositions.concat( - axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true) - ); - } - - } else { - for (pos = axis.min + (tickPositions[0] - axis.min) % minorTickInterval; pos <= axis.max; pos += minorTickInterval) { - minorTickPositions.push(pos); - } - } - - return minorTickPositions; - }, - - /** - * Adjust the min and max for the minimum range. Keep in mind that the series data is - * not yet processed, so we don't have information on data cropping and grouping, or - * updated axis.pointRange or series.pointRange. The data can't be processed until - * we have finally established min and max. - */ - adjustForMinRange: function () { - var axis = this, - options = axis.options, - min = axis.min, - max = axis.max, - zoomOffset, - spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange, - closestDataRange, - i, - distance, - xData, - loopLength, - minArgs, - maxArgs; - - // Set the automatic minimum range based on the closest point distance - if (axis.isXAxis && axis.minRange === UNDEFINED && !axis.isLog) { - - if (defined(options.min) || defined(options.max)) { - axis.minRange = null; // don't do this again - - } else { - - // Find the closest distance between raw data points, as opposed to - // closestPointRange that applies to processed points (cropped and grouped) - each(axis.series, function (series) { - xData = series.xData; - loopLength = series.xIncrement ? 1 : xData.length - 1; - for (i = loopLength; i > 0; i--) { - distance = xData[i] - xData[i - 1]; - if (closestDataRange === UNDEFINED || distance < closestDataRange) { - closestDataRange = distance; - } - } - }); - axis.minRange = mathMin(closestDataRange * 5, axis.dataMax - axis.dataMin); - } - } - - // if minRange is exceeded, adjust - if (max - min < axis.minRange) { - var minRange = axis.minRange; - zoomOffset = (minRange - max + min) / 2; - - // if min and max options have been set, don't go beyond it - minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)]; - if (spaceAvailable) { // if space is available, stay within the data range - minArgs[2] = axis.dataMin; - } - min = arrayMax(minArgs); - - maxArgs = [min + minRange, pick(options.max, min + minRange)]; - if (spaceAvailable) { // if space is availabe, stay within the data range - maxArgs[2] = axis.dataMax; - } - - max = arrayMin(maxArgs); - - // now if the max is adjusted, adjust the min back - if (max - min < minRange) { - minArgs[0] = max - minRange; - minArgs[1] = pick(options.min, max - minRange); - min = arrayMax(minArgs); - } - } - - // Record modified extremes - axis.min = min; - axis.max = max; - }, - - /** - * Update translation information - */ - setAxisTranslation: function () { - var axis = this, - range = axis.max - axis.min, - pointRange = 0, - closestPointRange, - seriesClosestPointRange, - transA = axis.transA; - - // adjust translation for padding - if (axis.isXAxis) { - if (axis.isLinked) { - pointRange = axis.linkedParent.pointRange; - } else { - each(axis.series, function (series) { - pointRange = mathMax(pointRange, series.pointRange); - seriesClosestPointRange = series.closestPointRange; - if (!series.noSharedTooltip && defined(seriesClosestPointRange)) { - closestPointRange = defined(closestPointRange) ? - mathMin(closestPointRange, seriesClosestPointRange) : - seriesClosestPointRange; - } - }); - } - - // pointRange means the width reserved for each point, like in a column chart - axis.pointRange = pointRange; - - // closestPointRange means the closest distance between points. In columns - // it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange - // is some other value - axis.closestPointRange = closestPointRange; - } - - // secondary values - axis.oldTransA = transA; - axis.translationSlope = axis.transA = transA = axis.len / ((range + pointRange) || 1); - axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend - axis.minPixelPadding = transA * (pointRange / 2); - }, - - /** - * Set the tick positions to round values and optionally extend the extremes - * to the nearest tick - */ - setTickPositions: function (secondPass) { - var axis = this, - chart = axis.chart, - options = axis.options, - isLog = axis.isLog, - isDatetimeAxis = axis.isDatetimeAxis, - isXAxis = axis.isXAxis, - isLinked = axis.isLinked, - tickPositioner = axis.options.tickPositioner, - magnitude, - maxPadding = options.maxPadding, - minPadding = options.minPadding, - length, - linkedParentExtremes, - tickIntervalOption = options.tickInterval, - tickPixelIntervalOption = options.tickPixelInterval, - tickPositions, - categories = axis.categories; - - // linked axis gets the extremes from the parent axis - if (isLinked) { - axis.linkedParent = chart[isXAxis ? 'xAxis' : 'yAxis'][options.linkedTo]; - linkedParentExtremes = axis.linkedParent.getExtremes(); - axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin); - axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax); - if (options.type !== axis.linkedParent.options.type) { - error(11, 1); // Can't link axes of different type - } - } else { // initial min and max from the extreme data values - axis.min = pick(axis.userMin, options.min, axis.dataMin); - axis.max = pick(axis.userMax, options.max, axis.dataMax); - } - - if (isLog) { - if (!secondPass && mathMin(axis.min, pick(axis.dataMin, axis.min)) <= 0) { // #978 - error(10, 1); // Can't plot negative values on log axis - } - axis.min = correctFloat(log2lin(axis.min)); // correctFloat cures #934 - axis.max = correctFloat(log2lin(axis.max)); - } - - // handle zoomed range - if (axis.range) { - axis.userMin = axis.min = mathMax(axis.min, axis.max - axis.range); // #618 - axis.userMax = axis.max; - if (secondPass) { - axis.range = null; // don't use it when running setExtremes - } - } - - // adjust min and max for the minimum range - axis.adjustForMinRange(); - - // pad the values to get clear of the chart's edges - if (!categories && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) { - length = (axis.max - axis.min) || 1; - if (!defined(options.min) && !defined(axis.userMin) && minPadding && (axis.dataMin < 0 || !axis.ignoreMinPadding)) { - axis.min -= length * minPadding; - } - if (!defined(options.max) && !defined(axis.userMax) && maxPadding && (axis.dataMax > 0 || !axis.ignoreMaxPadding)) { - axis.max += length * maxPadding; - } - } - - // get tickInterval - if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) { - axis.tickInterval = 1; - } else if (isLinked && !tickIntervalOption && - tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) { - axis.tickInterval = axis.linkedParent.tickInterval; - } else { - axis.tickInterval = pick( - tickIntervalOption, - categories ? // for categoried axis, 1 is default, for linear axis use tickPix - 1 : - (axis.max - axis.min) * tickPixelIntervalOption / (axis.len || 1) - ); - } - - // Now we're finished detecting min and max, crop and group series data. This - // is in turn needed in order to find tick positions in ordinal axes. - if (isXAxis && !secondPass) { - each(axis.series, function (series) { - series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax); - }); - } - - // set the translation factor used in translate function - axis.setAxisTranslation(); - - // hook for ordinal axes. To do: merge with below - if (axis.beforeSetTickPositions) { - axis.beforeSetTickPositions(); - } - - // hook for extensions, used in Highstock ordinal axes - if (axis.postProcessTickInterval) { - axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval); - } - - // for linear axes, get magnitude and normalize the interval - if (!isDatetimeAxis && !isLog) { // linear - magnitude = math.pow(10, mathFloor(math.log(axis.tickInterval) / math.LN10)); - if (!defined(options.tickInterval)) { - axis.tickInterval = normalizeTickInterval(axis.tickInterval, null, magnitude, options); - } - } - - // get minorTickInterval - axis.minorTickInterval = options.minorTickInterval === 'auto' && axis.tickInterval ? - axis.tickInterval / 5 : options.minorTickInterval; - - // find the tick positions - axis.tickPositions = tickPositions = options.tickPositions || (tickPositioner && tickPositioner.apply(axis, [axis.min, axis.max])); - if (!tickPositions) { - if (isDatetimeAxis) { - tickPositions = (axis.getNonLinearTimeTicks || getTimeTicks)( - normalizeTimeTickInterval(axis.tickInterval, options.units), - axis.min, - axis.max, - options.startOfWeek, - axis.ordinalPositions, - axis.closestPointRange, - true - ); - } else if (isLog) { - tickPositions = axis.getLogTickPositions(axis.tickInterval, axis.min, axis.max); - } else { - tickPositions = axis.getLinearTickPositions(axis.tickInterval, axis.min, axis.max); - } - axis.tickPositions = tickPositions; - } - - if (!isLinked) { - - // reset min/max or remove extremes based on start/end on tick - var roundedMin = tickPositions[0], - roundedMax = tickPositions[tickPositions.length - 1]; - - if (options.startOnTick) { - axis.min = roundedMin; - } else if (axis.min > roundedMin) { - tickPositions.shift(); - } - - if (options.endOnTick) { - axis.max = roundedMax; - } else if (axis.max < roundedMax) { - tickPositions.pop(); - } - - } - }, - - /** - * Set the max ticks of either the x and y axis collection - */ - setMaxTicks: function () { - - var chart = this.chart, - maxTicks = chart.maxTicks, - tickPositions = this.tickPositions, - xOrY = this.xOrY; - - if (!maxTicks) { // first call, or maxTicks have been reset after a zoom operation - maxTicks = { - x: 0, - y: 0 - }; - } - - if (!this.isLinked && !this.isDatetimeAxis && tickPositions.length > maxTicks[xOrY] && this.options.alignTicks !== false) { - maxTicks[xOrY] = tickPositions.length; - } - chart.maxTicks = maxTicks; - }, - - /** - * When using multiple axes, adjust the number of ticks to match the highest - * number of ticks in that group - */ - adjustTickAmount: function () { - var axis = this, - chart = axis.chart, - xOrY = axis.xOrY, - tickPositions = axis.tickPositions, - maxTicks = chart.maxTicks; - - if (maxTicks && maxTicks[xOrY] && !axis.isDatetimeAxis && !axis.categories && !axis.isLinked && axis.options.alignTicks !== false) { // only apply to linear scale - var oldTickAmount = axis.tickAmount, - calculatedTickAmount = tickPositions.length, - tickAmount; - - // set the axis-level tickAmount to use below - axis.tickAmount = tickAmount = maxTicks[xOrY]; - - if (calculatedTickAmount < tickAmount) { - while (tickPositions.length < tickAmount) { - tickPositions.push(correctFloat( - tickPositions[tickPositions.length - 1] + axis.tickInterval - )); - } - axis.transA *= (calculatedTickAmount - 1) / (tickAmount - 1); - axis.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 - * - */ - setScale: function () { - var axis = this, - stacks = axis.stacks, - type, - i, - isDirtyData, - isDirtyAxisLength; - - axis.oldMin = axis.min; - axis.oldMax = axis.max; - axis.oldAxisLength = axis.len; - - // set the new axisLength - axis.setAxisSize(); - //axisLength = horiz ? axisWidth : axisHeight; - isDirtyAxisLength = axis.len !== axis.oldAxisLength; - - // is there new data? - each(axis.series, function (series) { - if (series.isDirtyData || series.isDirty || - series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well - isDirtyData = true; - } - }); - - // do we really need to go through all this? - if (isDirtyAxisLength || isDirtyData || axis.isLinked || - axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax) { - - // get data extremes if needed - axis.getSeriesExtremes(); - - // get fixed positions based on tickInterval - axis.setTickPositions(); - - // record old values to decide whether a rescale is necessary later on (#540) - axis.oldUserMin = axis.userMin; - axis.oldUserMax = axis.userMax; - - // Mark as dirty if it is not already set to dirty and extremes have changed. #595. - if (!axis.isDirty) { - axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax; - } - } - - - // reset stacks - if (!axis.isXAxis) { - for (type in stacks) { - for (i in stacks[type]) { - stacks[type][i].cum = stacks[type][i].total; - } - } - } - - // Set the maximum tick amount - axis.setMaxTicks(); - }, - - /** - * 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 - * @param {Object} eventArguments - * - */ - setExtremes: function (newMin, newMax, redraw, animation, eventArguments) { - var axis = this, - chart = axis.chart; - - redraw = pick(redraw, true); // defaults to true - - // Extend the arguments with min and max - eventArguments = extend(eventArguments, { - min: newMin, - max: newMax - }); - - // Fire the event - fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler - - axis.userMin = newMin; - axis.userMax = newMax; - - // Mark for running afterSetExtremes - axis.isDirtyExtremes = true; - - // redraw - if (redraw) { - chart.redraw(animation); - } - }); - }, - - /** - * Update the axis metrics - */ - setAxisSize: function () { - var axis = this, - chart = axis.chart, - options = axis.options; - - var offsetLeft = options.offsetLeft || 0, - offsetRight = options.offsetRight || 0; - - // basic values - // expose to use in Series object and navigator - axis.left = pick(options.left, chart.plotLeft + offsetLeft); - axis.top = pick(options.top, chart.plotTop); - axis.width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight); - axis.height = pick(options.height, chart.plotHeight); - axis.bottom = chart.chartHeight - axis.height - axis.top; - axis.right = chart.chartWidth - axis.width - axis.left; - axis.len = mathMax(axis.horiz ? axis.width : axis.height, 0); // mathMax fixes #905 - }, - - /** - * Get the actual axis extremes - */ - getExtremes: function () { - var axis = this, - isLog = axis.isLog; - - return { - min: isLog ? correctFloat(lin2log(axis.min)) : axis.min, - max: isLog ? correctFloat(lin2log(axis.max)) : axis.max, - dataMin: axis.dataMin, - dataMax: axis.dataMax, - userMin: axis.userMin, - userMax: axis.userMax - }; - }, - - /** - * Get the zero plane either based on zero or on the min or max value. - * Used in bar and area plots - */ - getThreshold: function (threshold) { - var axis = this, - isLog = axis.isLog; - - var realMin = isLog ? lin2log(axis.min) : axis.min, - realMax = isLog ? lin2log(axis.max) : axis.max; - - if (realMin > threshold || threshold === null) { - threshold = realMin; - } else if (realMax < threshold) { - threshold = realMax; - } - - return axis.translate(threshold, 0, 1, 0, 1); - }, - - /** - * Add a plot band or plot line after render time - * - * @param options {Object} The plotBand or plotLine configuration object - */ - addPlotBandOrLine: function (options) { - var obj = new PlotLineOrBand(this, options).render(); - this.plotLinesAndBands.push(obj); - return obj; - }, - - /** - * Render the tick labels to a preliminary position to get their sizes - */ - getOffset: function () { - var axis = this, - chart = axis.chart, - renderer = chart.renderer, - options = axis.options, - tickPositions = axis.tickPositions, - ticks = axis.ticks, - horiz = axis.horiz, - side = axis.side, - hasData, - showAxis, - titleOffset = 0, - titleOffsetOption, - titleMargin = 0, - axisTitleOptions = options.title, - labelOptions = options.labels, - labelOffset = 0, // reset - axisOffset = chart.axisOffset, - directionFactor = [-1, 1, 1, -1][side], - n; - - - // For reuse in Axis.render - axis.hasData = hasData = axis.series.length && defined(axis.min) && defined(axis.max); - axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); - - // Create the axisGroup and gridGroup elements on first iteration - if (!axis.axisGroup) { - axis.axisGroup = renderer.g('axis') - .attr({ zIndex: options.zIndex || 7 }) - .add(); - axis.gridGroup = renderer.g('grid') - .attr({ zIndex: options.gridZIndex || 1 }) - .add(); - } - - if (hasData || axis.isLinked) { - each(tickPositions, function (pos) { - if (!ticks[pos]) { - ticks[pos] = new Tick(axis, pos); - } else { - ticks[pos].addLabel(); // update labels depending on tick interval - } - - }); - - each(tickPositions, function (pos) { - // 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 (axis.staggerLines) { - labelOffset += (axis.staggerLines - 1) * 16; - } - - } else { // doesn't have data - for (n in ticks) { - ticks[n].destroy(); - delete ticks[n]; - } - } - - if (axisTitleOptions && axisTitleOptions.text) { - if (!axis.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(axis.axisGroup); - axis.axisTitle.isNew = true; - } - - if (showAxis) { - titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width']; - titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10); - titleOffsetOption = axisTitleOptions.offset; - } - - // hide or show the title depending on whether showEmpty is set - axis.axisTitle[showAxis ? 'show' : 'hide'](); - } - - // handle automatic or user set offset - axis.offset = directionFactor * pick(options.offset, axisOffset[side]); - - - axis.axisTitleMargin = - pick(titleOffsetOption, - labelOffset + titleMargin + - (side !== 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x']) - ); - - axisOffset[side] = mathMax( - axisOffset[side], - axis.axisTitleMargin + titleOffset + directionFactor * axis.offset - ); - - }, - - /** - * Get the path for the axis line - */ - getLinePath: function (lineWidth) { - var chart = this.chart, - opposite = this.opposite, - offset = this.offset, - horiz = this.horiz, - lineLeft = this.left + (opposite ? this.width : 0) + offset, - lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset; - - return chart.renderer.crispLine([ - M, - horiz ? - this.left : - lineLeft, - horiz ? - lineTop : - this.top, - L, - horiz ? - chart.chartWidth - this.right : - lineLeft, - horiz ? - lineTop : - chart.chartHeight - this.bottom - ], lineWidth); - }, - - /** - * Position the title - */ - getTitlePosition: function () { - // compute anchor points for each of the title align options - var horiz = this.horiz, - axisLeft = this.left, - axisTop = this.top, - axisLength = this.len, - axisTitleOptions = this.options.title, - margin = horiz ? axisLeft : axisTop, - opposite = this.opposite, - offset = this.offset, - 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 ? axisTop + this.height : axisLeft) + - (horiz ? 1 : -1) * // horizontal axis reverses the margin - (opposite ? -1 : 1) * // so does opposite axes - this.axisTitleMargin + - (this.side === 2 ? fontSize : 0); - - return { - x: horiz ? - alongAxis : - offAxis + (opposite ? this.width : 0) + offset + - (axisTitleOptions.x || 0), // x - y: horiz ? - offAxis - (opposite ? this.height : 0) + offset : - alongAxis + (axisTitleOptions.y || 0) // y - }; - }, - - /** - * Render the axis - */ - render: function () { - var axis = this, - chart = axis.chart, - renderer = chart.renderer, - options = axis.options, - isLog = axis.isLog, - isLinked = axis.isLinked, - tickPositions = axis.tickPositions, - axisTitle = axis.axisTitle, - stacks = axis.stacks, - ticks = axis.ticks, - minorTicks = axis.minorTicks, - alternateBands = axis.alternateBands, - stackLabelOptions = options.stackLabels, - alternateGridColor = options.alternateGridColor, - lineWidth = options.lineWidth, - linePath, - hasRendered = chart.hasRendered, - slideInTicks = hasRendered && defined(axis.oldMin) && !isNaN(axis.oldMin), - hasData = axis.hasData, - showAxis = axis.showAxis, - from, - to; - - // If the series has data draw the ticks. Else only the line and title - if (hasData || isLinked) { - - // minor ticks - if (axis.minorTickInterval && !axis.categories) { - each(axis.getMinorTickPositions(), function (pos) { - if (!minorTicks[pos]) { - minorTicks[pos] = new Tick(axis, pos, 'minor'); - } - - // 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. Pull out the first item and render it last so that - // we can get the position of the neighbour label. #808. - each(tickPositions.slice(1).concat([tickPositions[0]]), function (pos, i) { - - // Reorganize the indices - i = (i === tickPositions.length - 1) ? 0 : i + 1; - - // linked axes need an extra check to find out if - if (!isLinked || (pos >= axis.min && pos <= axis.max)) { - - if (!ticks[pos]) { - ticks[pos] = new Tick(axis, pos); - } - - // 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 < axis.max) { - if (!alternateBands[pos]) { - alternateBands[pos] = new PlotLineOrBand(axis); - } - from = pos; - to = tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] : axis.max; - alternateBands[pos].options = { - from: isLog ? lin2log(from) : from, - to: isLog ? lin2log(to) : to, - color: alternateGridColor - }; - alternateBands[pos].render(); - alternateBands[pos].isActive = true; - } - }); - } - - // custom plot lines and bands - if (!axis._addedPlotLB) { // only first time - each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) { - //plotLinesAndBands.push(new PlotLineOrBand(plotLineOptions).render()); - axis.addPlotBandOrLine(plotLineOptions); - }); - axis._addedPlotLB = true; - } - - } // 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) { - linePath = axis.getLinePath(lineWidth); - if (!axis.axisLine) { - axis.axisLine = renderer.path(linePath) - .attr({ - stroke: options.lineColor, - 'stroke-width': lineWidth, - zIndex: 7 - }) - .add(); - } else { - axis.axisLine.animate({ d: linePath }); - } - - // show or hide the line depending on options.showEmpty - axis.axisLine[showAxis ? 'show' : 'hide'](); - - } - - if (axisTitle && showAxis) { - - axisTitle[axisTitle.isNew ? 'attr' : 'animate']( - axis.getTitlePosition() - ); - 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 - }) - .add(); - } - - // plotLeft/Top will change when y axis gets wider so we need to translate the - // stackTotalGroup at every render call. See bug #506 and #516 - stackTotalGroup.translate(chart.plotLeft, chart.plotTop); - - // 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 - */ - removePlotBandOrLine: function (id) { - var plotLinesAndBands = this.plotLinesAndBands, - i = plotLinesAndBands.length; - while (i--) { - if (plotLinesAndBands[i].id === id) { - plotLinesAndBands[i].destroy(); - } - } - }, - - /** - * Update the axis title by options - */ - setTitle: function (newTitleOptions, redraw) { - var axis = this, - chart = axis.chart, - options = axis.options, - axisTitle; - - options.title = merge(options.title, newTitleOptions); - - axis.axisTitle = axisTitle && axisTitle.destroy(); // #922 - axis.isDirty = true; - - if (pick(redraw, true)) { - chart.redraw(); - } - }, - - /** - * Redraw the axis to reflect changes in the data or axis extremes - */ - redraw: function () { - var axis = this, - chart = axis.chart; - - // hide tooltip and hover states - if (chart.tracker.resetTracker) { - chart.tracker.resetTracker(true); - } - - // render the axis - axis.render(); - - // move plot lines and bands - each(axis.plotLinesAndBands, function (plotLine) { - plotLine.render(); - }); - - // mark associated series as dirty and ready for redraw - each(axis.series, function (series) { - series.isDirty = true; - }); - - }, - - /** - * Set new axis categories and optionally redraw - * @param {Array} newCategories - * @param {Boolean} doRedraw - */ - setCategories: function (newCategories, doRedraw) { - var axis = this, - chart = axis.chart; - - // set the categories - axis.categories = axis.userOptions.categories = newCategories; - - // force reindexing tooltips - each(axis.series, function (series) { - series.translate(); - series.setTooltipPoints(true); - }); - - - // optionally redraw - axis.isDirty = true; - - if (pick(doRedraw, true)) { - chart.redraw(); - } - }, - - /** - * Destroys an Axis instance. - */ - destroy: function () { - var axis = this, - stacks = axis.stacks, - stackKey; - - // Remove the events - removeEvent(axis); - - // Destroy each stack total - for (stackKey in stacks) { - destroyObjectProperties(stacks[stackKey]); - - stacks[stackKey] = null; - } - - // Destroy collections - each([axis.ticks, axis.minorTicks, axis.alternateBands, axis.plotLinesAndBands], function (coll) { - destroyObjectProperties(coll); - }); - - // Destroy local variables - each(['stackTotalGroup', 'axisLine', 'axisGroup', 'gridGroup', 'axisTitle'], function (prop) { - if (axis[prop]) { - axis[prop] = axis[prop].destroy(); - } - }); - } - - -}; // end Axis - -/** - * The tooltip object - * @param {Object} chart The chart instance - * @param {Object} options Tooltip options - */ -function Tooltip(chart, options) { - var borderWidth = options.borderWidth, - style = options.style, - shared = options.shared, - padding = pInt(style.padding); - - // Save the chart and options - this.chart = chart; - this.options = options; - - // remove padding CSS and apply padding on box instead - style.padding = 0; - - // Keep track of the current series - //this.currentSeries = UNDEFINED; - - // List of crosshairs - this.crosshairs = []; - - // Current values of x and y when animating - this.currentX = 0; - this.currentY = 0; - - // The tooltipTick function, initialized to nothing - //this.tooltipTick = UNDEFINED; - - // The tooltip is initially hidden - this.tooltipIsHidden = true; - - // create the label - this.label = chart.renderer.label('', 0, 0, null, null, null, options.useHTML, null, 'tooltip') - .attr({ - padding: padding, - fill: options.backgroundColor, - 'stroke-width': borderWidth, - r: options.borderRadius, - zIndex: 8 - }) - .css(style) - .hide() - .add(); - - // When using canVG the shadow shows up as a gray circle - // even if the tooltip is hidden. - if (!useCanVG) { - this.label.shadow(options.shadow); - } - - // Public property for getting the shared state. - this.shared = shared; -} - -Tooltip.prototype = { - /** - * Destroy the tooltip and its elements. - */ - destroy: function () { - each(this.crosshairs, function (crosshair) { - if (crosshair) { - crosshair.destroy(); - } - }); - - // Destroy and clear local variables - if (this.label) { - this.label = this.label.destroy(); - } - }, - - /** - * Provide a soft movement for the tooltip - * - * @param {Number} finalX - * @param {Number} finalY - * @private - */ - move: function (finalX, finalY) { - var tooltip = this; - - // get intermediate values for animation - tooltip.currentX = tooltip.tooltipIsHidden ? finalX : (2 * tooltip.currentX + finalX) / 3; - tooltip.currentY = tooltip.tooltipIsHidden ? finalY : (tooltip.currentY + finalY) / 2; - - // move to the intermediate value - tooltip.label.attr({ x: tooltip.currentX, y: tooltip.currentY }); - - // run on next tick of the mouse tracker - if (mathAbs(finalX - tooltip.currentX) > 1 || mathAbs(finalY - tooltip.currentY) > 1) { - tooltip.tooltipTick = function () { - tooltip.move(finalX, finalY); - }; - } else { - tooltip.tooltipTick = null; - } - }, - - /** - * Hide the tooltip - */ - hide: function () { - if (!this.tooltipIsHidden) { - var hoverPoints = this.chart.hoverPoints; - - this.label.hide(); - - // hide previous hoverPoints and set new - if (hoverPoints) { - each(hoverPoints, function (point) { - point.setState(); - }); - } - - this.chart.hoverPoints = null; - this.tooltipIsHidden = true; - } - }, - - /** - * Hide the crosshairs - */ - hideCrosshairs: function () { - each(this.crosshairs, function (crosshair) { - if (crosshair) { - crosshair.hide(); - } - }); - }, - - /** - * Extendable method to get the anchor position of the tooltip - * from a point or set of points - */ - getAnchor: function (points, mouseEvent) { - var ret, - chart = this.chart, - inverted = chart.inverted, - plotX = 0, - plotY = 0; - - points = splat(points); - - // Pie uses a special tooltipPos - ret = points[0].tooltipPos; - - // When shared, use the average position - if (!ret) { - each(points, function (point) { - plotX += point.plotX; - plotY += point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY; - }); - - plotX /= points.length; - plotY /= points.length; - - ret = [ - inverted ? chart.plotWidth - plotY : plotX, - this.shared && !inverted && points.length > 1 && mouseEvent ? - mouseEvent.chartY - chart.plotTop : // place shared tooltip next to the mouse (#424) - inverted ? chart.plotHeight - plotX : plotY - ]; - } - - return map(ret, mathRound); - }, - - /** - * Place the tooltip in a chart without spilling over - * and not covering the point it self. - */ - getPosition: function (boxWidth, boxHeight, point) { - - // Set up the variables - var chart = this.chart, - plotLeft = chart.plotLeft, - plotTop = chart.plotTop, - plotWidth = chart.plotWidth, - plotHeight = chart.plotHeight, - distance = pick(this.options.distance, 12), - pointX = point.plotX, - pointY = point.plotY, - x = pointX + plotLeft + (chart.inverted ? distance : -boxWidth - distance), - y = pointY - boxHeight + plotTop + 15, // 15 means the point is 15 pixels up from the bottom of the tooltip - alignedRight; - - // It is too far to the left, adjust it - if (x < 7) { - x = plotLeft + pointX + distance; - } - - // Test to see if the tooltip is too far to the right, - // if it is, move it back to be inside and then up to not cover the point. - if ((x + boxWidth) > (plotLeft + plotWidth)) { - x -= (x + boxWidth) - (plotLeft + plotWidth); - y = pointY - boxHeight + plotTop - distance; - alignedRight = true; - } - - // If it is now above the plot area, align it to the top of the plot area - if (y < plotTop + 5) { - y = plotTop + 5; - - // If the tooltip is still covering the point, move it below instead - if (alignedRight && pointY >= y && pointY <= (y + boxHeight)) { - y = pointY + plotTop + distance; // below - } - } - - // Now if the tooltip is below the chart, move it up. It's better to cover the - // point than to disappear outside the chart. #834. - if (y + boxHeight > plotTop + plotHeight) { - y = mathMax(plotTop, plotTop + plotHeight - boxHeight - distance); // below - } - - - return {x: x, y: y}; - }, - - /** - * Refresh the tooltip's text and position. - * @param {Object} point - */ - refresh: function (point, mouseEvent) { - var tooltip = this, - chart = tooltip.chart, - label = tooltip.label, - options = tooltip.options; - - /** - * In case no user defined formatter is given, this will be used - */ - function defaultFormatter() { - var pThis = this, - items = pThis.points || splat(pThis), - series = items[0].series, - s; - - // build the header - s = [series.tooltipHeaderFormatter(items[0].key)]; - - // build the values - each(items, function (item) { - series = item.series; - s.push((series.tooltipFormatter && series.tooltipFormatter(item)) || - item.point.tooltipFormatter(series.tooltipOptions.pointFormat)); - }); - - // footer - s.push(options.footerFormat || ''); - - return s.join(''); - } - - var x, - y, - show, - anchor, - textConfig = {}, - text, - pointConfig = [], - formatter = options.formatter || defaultFormatter, - hoverPoints = chart.hoverPoints, - placedTooltipPoint, - borderColor, - crosshairsOptions = options.crosshairs, - shared = tooltip.shared, - currentSeries; - - // get the reference point coordinates (pie charts use tooltipPos) - anchor = tooltip.getAnchor(point, mouseEvent); - x = anchor[0]; - y = anchor[1]; - - // shared tooltip, array is sent over - if (shared && !(point.series && point.series.noSharedTooltip)) { - - // hide previous hoverPoints and set new - if (hoverPoints) { - each(hoverPoints, function (point) { - point.setState(); - }); - } - chart.hoverPoints = point; - - each(point, function (item) { - item.setState(HOVER_STATE); - - pointConfig.push(item.getLabelConfig()); - }); - - textConfig = { - x: point[0].category, - y: point[0].y - }; - textConfig.points = pointConfig; - point = point[0]; - - // single point tooltip - } else { - textConfig = point.getLabelConfig(); - } - text = formatter.call(textConfig); - - // register the current series - currentSeries = point.series; - - - // For line type series, hide tooltip if the point falls outside the plot - show = shared || !currentSeries.isCartesian || currentSeries.tooltipOutsidePlot || chart.isInsidePlot(x, y); - - // update the inner HTML - if (text === false || !show) { - this.hide(); - } else { - - // show it - if (tooltip.tooltipIsHidden) { - label.show(); - } - - // update text - label.attr({ - text: text - }); - - // set the stroke color of the box - borderColor = options.borderColor || point.color || currentSeries.color || '#606060'; - label.attr({ - stroke: borderColor - }); - - placedTooltipPoint = (options.positioner || tooltip.getPosition).call( - tooltip, - label.width, - label.height, - { plotX: x, plotY: y } - ); - - // do the move - tooltip.move(mathRound(placedTooltipPoint.x), mathRound(placedTooltipPoint.y)); - - - tooltip.tooltipIsHidden = false; - } - - // 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( - i ? pick(point.stackY, point.y) : point.x, // #814 - 1 - ); - - if (tooltip.crosshairs[i]) { - tooltip.crosshairs[i].attr({ d: path, visibility: VISIBLE }); - } else { - attribs = { - 'stroke-width': crosshairsOptions[i].width || 1, - stroke: crosshairsOptions[i].color || '#C0C0C0', - zIndex: crosshairsOptions[i].zIndex || 2 - }; - if (crosshairsOptions[i].dashStyle) { - attribs.dashstyle = crosshairsOptions[i].dashStyle; - } - tooltip.crosshairs[i] = chart.renderer.path(path) - .attr(attribs) - .add(); - } - } - } - } - fireEvent(chart, 'tooltipRefresh', { - text: text, - x: x + chart.plotLeft, - y: y + chart.plotTop, - borderColor: borderColor - }); - }, - - /** - * Runs the tooltip animation one tick. - */ - tick: function () { - if (this.tooltipTick) { - this.tooltipTick(); - } - } -}; -/** - * The mouse tracker object - * @param {Object} chart The Chart instance - * @param {Object} options The root options object - */ -function MouseTracker(chart, options) { - var zoomType = useCanVG ? '' : options.chart.zoomType; - - // Zoom status - this.zoomX = /x/.test(zoomType); - this.zoomY = /y/.test(zoomType); - - // Store reference to options - this.options = options; - - // Reference to the chart - this.chart = chart; - - // The interval id - //this.tooltipInterval = UNDEFINED; - - // The cached x hover position - //this.hoverX = UNDEFINED; - - // The chart position - //this.chartPosition = UNDEFINED; - - // The selection marker element - //this.selectionMarker = UNDEFINED; - - // False or a value > 0 if a dragging operation - //this.mouseDownX = UNDEFINED; - //this.mouseDownY = UNDEFINED; - this.init(chart, options.tooltip); -} - -MouseTracker.prototype = { - /** - * Add crossbrowser support for chartX and chartY - * @param {Object} e The event object in standard browsers - */ - normalizeMouseEvent: function (e) { - var chartPosition, - chartX, - chartY, - ePos; - - // common IE normalizing - e = e || win.event; - if (!e.target) { - e.target = e.srcElement; - } - - // jQuery only copies over some properties. IE needs e.x and iOS needs touches. - if (e.originalEvent) { - e = e.originalEvent; - } - - // The same for MooTools. It renames e.pageX to e.page.x. #445. - if (e.event) { - e = e.event; - } - - // iOS - ePos = e.touches ? e.touches.item(0) : e; - - // get mouse position - this.chartPosition = chartPosition = offset(this.chart.container); - - // chartX and chartY - if (ePos.pageX === UNDEFINED) { // IE < 9. #886. - chartX = e.x; - chartY = e.y; - } else { - chartX = ePos.pageX - chartPosition.left; - chartY = ePos.pageY - chartPosition.top; - } - - return extend(e, { - chartX: mathRound(chartX), - chartY: mathRound(chartY) - - }); - }, - - /** - * Get the click position in terms of axis values. - * - * @param {Object} e A mouse event - */ - getMouseCoordinates: function (e) { - var coordinates = { - xAxis: [], - yAxis: [] - }, - chart = this.chart; - - each(chart.axes, function (axis) { - var isXAxis = axis.isXAxis, - isHorizontal = chart.inverted ? !isXAxis : isXAxis; - - coordinates[isXAxis ? 'xAxis' : 'yAxis'].push({ - axis: axis, - value: axis.translate( - isHorizontal ? - e.chartX - chart.plotLeft : - chart.plotHeight - e.chartY + chart.plotTop, - true - ) - }); - }); - return coordinates; - }, - - /** - * With line type charts with a single tracker, get the point closest to the mouse - */ - onmousemove: function (e) { - var mouseTracker = this, - chart = mouseTracker.chart, - series = chart.series, - point, - points, - hoverPoint = chart.hoverPoint, - hoverSeries = chart.hoverSeries, - i, - j, - distance = chart.chartWidth, - // the index in the tooltipPoints array, corresponding to pixel position in plot area - index = chart.inverted ? chart.plotHeight + chart.plotTop - e.chartY : e.chartX - chart.plotLeft; - - // shared tooltip - if (chart.tooltip && mouseTracker.options.tooltip.shared && !(hoverSeries && hoverSeries.noSharedTooltip)) { - 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].options.enableMouseTracking !== false && - !series[j].noSharedTooltip && 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 !== mouseTracker.hoverX)) { - chart.tooltip.refresh(points, e); - mouseTracker.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 - */ - resetTracker: function (allowMove) { - var mouseTracker = this, - chart = mouseTracker.chart, - hoverSeries = chart.hoverSeries, - hoverPoint = chart.hoverPoint, - tooltipPoints = chart.hoverPoints || hoverPoint, - tooltip = chart.tooltip; - - // Narrow in allowMove - allowMove = allowMove && tooltip && tooltipPoints; - - // Check if the points have moved outside the plot area, #1003 - if (allowMove && splat(tooltipPoints)[0].plotX === UNDEFINED) { - allowMove = false; - } - - // Just move the tooltip, #349 - if (allowMove) { - tooltip.refresh(tooltipPoints); - - // Full reset - } else { - - if (hoverPoint) { - hoverPoint.onMouseOut(); - } - - if (hoverSeries) { - hoverSeries.onMouseOut(); - } - - if (tooltip) { - tooltip.hide(); - tooltip.hideCrosshairs(); - } - - mouseTracker.hoverX = null; - - } - }, - - /** - * Set the JS events on the container element - */ - setDOMEvents: function () { - var lastWasOutsidePlot = true, - mouseTracker = this, - chart = mouseTracker.chart, - container = chart.container, - hasDragged, - zoomHor = (mouseTracker.zoomX && !chart.inverted) || (mouseTracker.zoomY && chart.inverted), - zoomVert = (mouseTracker.zoomY && !chart.inverted) || (mouseTracker.zoomX && chart.inverted); - - /** - * Mouse up or outside the plot area - */ - function drop() { - if (mouseTracker.selectionMarker) { - var selectionData = { - xAxis: [], - yAxis: [] - }, - selectionBox = mouseTracker.selectionMarker.getBBox(), - selectionLeft = selectionBox.x - chart.plotLeft, - selectionTop = selectionBox.y - chart.plotTop, - runZoom; - - // a selection has been made - if (hasDragged) { - - // record each axis' min and max - each(chart.axes, function (axis) { - if (axis.options.zoomEnabled !== false) { - var isXAxis = axis.isXAxis, - isHorizontal = chart.inverted ? !isXAxis : isXAxis, - selectionMin = axis.translate( - isHorizontal ? - selectionLeft : - chart.plotHeight - selectionTop - selectionBox.height, - true, - 0, - 0, - 1 - ), - selectionMax = axis.translate( - isHorizontal ? - selectionLeft + selectionBox.width : - chart.plotHeight - selectionTop, - true, - 0, - 0, - 1 - ); - - if (!isNaN(selectionMin) && !isNaN(selectionMax)) { // #859 - selectionData[isXAxis ? 'xAxis' : 'yAxis'].push({ - axis: axis, - min: mathMin(selectionMin, selectionMax), // for reversed axes, - max: mathMax(selectionMin, selectionMax) - }); - runZoom = true; - } - } - }); - if (runZoom) { - fireEvent(chart, 'selection', selectionData, function (args) { chart.zoom(args); }); - } - - } - mouseTracker.selectionMarker = mouseTracker.selectionMarker.destroy(); - } - - if (chart) { // it may be destroyed on mouse up - #877 - css(container, { cursor: 'auto' }); - chart.cancelClick = hasDragged; // #370 - chart.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. - */ - mouseTracker.hideTooltipOnMouseMove = function (e) { - - // Get e.pageX and e.pageY back in MooTools - washMouseEvent(e); - - // If we're outside, hide the tooltip - if (mouseTracker.chartPosition && chart.hoverSeries && chart.hoverSeries.isCartesian && - !chart.isInsidePlot(e.pageX - mouseTracker.chartPosition.left - chart.plotLeft, - e.pageY - mouseTracker.chartPosition.top - chart.plotTop)) { - mouseTracker.resetTracker(); - } - }; - - /** - * When mouse leaves the container, hide the tooltip. - */ - mouseTracker.hideTooltipOnMouseLeave = function () { - mouseTracker.resetTracker(); - mouseTracker.chartPosition = null; // also reset the chart position, used in #149 fix - }; - - - /* - * Record the starting position of a dragoperation - */ - container.onmousedown = function (e) { - e = mouseTracker.normalizeMouseEvent(e); - - // issue #295, dragging not always working in Firefox - if (!hasTouch && e.preventDefault) { - e.preventDefault(); - } - - // record the start position - chart.mouseIsDown = true; - chart.cancelClick = false; - chart.mouseDownX = mouseTracker.mouseDownX = e.chartX; - mouseTracker.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 = mouseTracker.normalizeMouseEvent(e); - if (!hasTouch) { // not for touch devices - e.returnValue = false; - } - - var chartX = e.chartX, - chartY = e.chartY, - isOutsidePlot = !chart.isInsidePlot(chartX - chart.plotLeft, chartY - chart.plotTop); - - // on touch devices, only trigger click if a handler is defined - if (hasTouch && e.type === 'touchstart') { - if (attr(e.target, 'isTracker')) { - if (!chart.runTrackerClick) { - e.preventDefault(); - } - } else if (!chart.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 < chart.plotLeft) { - chartX = chart.plotLeft; - } else if (chartX > chart.plotLeft + chart.plotWidth) { - chartX = chart.plotLeft + chart.plotWidth; - } - - if (chartY < chart.plotTop) { - chartY = chart.plotTop; - } else if (chartY > chart.plotTop + chart.plotHeight) { - chartY = chart.plotTop + chart.plotHeight; - } - } - - if (chart.mouseIsDown && e.type !== 'touchstart') { // make selection - - // determine if the mouse has moved more than 10px - hasDragged = Math.sqrt( - Math.pow(mouseTracker.mouseDownX - chartX, 2) + - Math.pow(mouseTracker.mouseDownY - chartY, 2) - ); - if (hasDragged > 10) { - var clickedInside = chart.isInsidePlot(mouseTracker.mouseDownX - chart.plotLeft, mouseTracker.mouseDownY - chart.plotTop); - - // make a selection - if (chart.hasCartesianSeries && (mouseTracker.zoomX || mouseTracker.zoomY) && clickedInside) { - if (!mouseTracker.selectionMarker) { - mouseTracker.selectionMarker = chart.renderer.rect( - chart.plotLeft, - chart.plotTop, - zoomHor ? 1 : chart.plotWidth, - zoomVert ? 1 : chart.plotHeight, - 0 - ) - .attr({ - fill: mouseTracker.options.chart.selectionMarkerFill || 'rgba(69,114,167,0.25)', - zIndex: 7 - }) - .add(); - } - } - - // adjust the width of the selection marker - if (mouseTracker.selectionMarker && zoomHor) { - var xSize = chartX - mouseTracker.mouseDownX; - mouseTracker.selectionMarker.attr({ - width: mathAbs(xSize), - x: (xSize > 0 ? 0 : xSize) + mouseTracker.mouseDownX - }); - } - // adjust the height of the selection marker - if (mouseTracker.selectionMarker && zoomVert) { - var ySize = chartY - mouseTracker.mouseDownY; - mouseTracker.selectionMarker.attr({ - height: mathAbs(ySize), - y: (ySize > 0 ? 0 : ySize) + mouseTracker.mouseDownY - }); - } - - // panning - if (clickedInside && !mouseTracker.selectionMarker && mouseTracker.options.chart.panning) { - chart.pan(chartX); - } - } - - } else if (!isOutsidePlot) { - // show the tooltip - mouseTracker.onmousemove(e); - } - - lastWasOutsidePlot = isOutsidePlot; - - // when outside plot, allow touch-drag by returning true - return isOutsidePlot || !chart.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', mouseTracker.hideTooltipOnMouseLeave); - - // 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', mouseTracker.hideTooltipOnMouseMove); - - container.ontouchstart = function (e) { - // For touch devices, use touchmove to zoom - if (mouseTracker.zoomX || mouseTracker.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) { - mouseTracker.resetTracker(); - } - }; - - - // MooTools 1.2.3 doesn't fire this in IE when using addEvent - container.onclick = function (e) { - var hoverPoint = chart.hoverPoint, - plotX, - plotY; - e = mouseTracker.normalizeMouseEvent(e); - - e.cancelBubble = true; // IE specific - - - if (!chart.cancelClick) { - // Detect clicks on trackers or tracker groups, #783 - if (hoverPoint && (attr(e.target, 'isTracker') || attr(e.target.parentNode, 'isTracker'))) { - plotX = hoverPoint.plotX; - plotY = hoverPoint.plotY; - - // add page position info - extend(hoverPoint, { - pageX: mouseTracker.chartPosition.left + chart.plotLeft + - (chart.inverted ? chart.plotWidth - plotY : plotX), - pageY: mouseTracker.chartPosition.top + chart.plotTop + - (chart.inverted ? chart.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, mouseTracker.getMouseCoordinates(e)); - - // fire a click event in the chart - if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { - fireEvent(chart, 'click', e); - } - } - - - } - }; - - }, - - /** - * Destroys the MouseTracker object and disconnects DOM events. - */ - destroy: function () { - var mouseTracker = this, - chart = mouseTracker.chart, - container = chart.container; - - // Destroy the tracker group element - if (chart.trackerGroup) { - chart.trackerGroup = chart.trackerGroup.destroy(); - } - - removeEvent(container, 'mouseleave', mouseTracker.hideTooltipOnMouseLeave); - removeEvent(doc, 'mousemove', mouseTracker.hideTooltipOnMouseMove); - container.onclick = container.onmousedown = container.onmousemove = container.ontouchstart = container.ontouchend = container.ontouchmove = null; - - // memory and CPU leak - clearInterval(this.tooltipInterval); - }, - - // Run MouseTracker - init: function (chart, options) { - if (!chart.trackerGroup) { - chart.trackerGroup = chart.renderer.g('tracker') - .attr({ zIndex: 9 }) - .add(); - } - - if (options.enabled) { - chart.tooltip = new Tooltip(chart, options); - - // set the fixed interval ticking for the smooth tooltip - this.tooltipInterval = setInterval(function () { chart.tooltip.tick(); }, 32); - } - - this.setDOMEvents(); - } -}; -/** - * The overview of the chart's series - */ -function Legend(chart) { - - this.init(chart); -} - -Legend.prototype = { - - /** - * Initialize the legend - */ - init: function (chart) { - var legend = this, - options = legend.options = chart.options.legend; - - if (!options.enabled) { - return; - } - - var //style = options.style || {}, // deprecated - itemStyle = options.itemStyle, - padding = pick(options.padding, 8), - itemMarginTop = options.itemMarginTop || 0; - - legend.baseline = pInt(itemStyle.fontSize) + 3 + itemMarginTop; // used in Series prototype - legend.itemStyle = itemStyle; - legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle); - legend.itemMarginTop = itemMarginTop; - legend.padding = padding; - legend.initialItemX = padding; - legend.initialItemY = padding - 5; // 5 is the number of pixels above the text - legend.maxItemWidth = 0; - legend.chart = chart; - //legend.allItems = UNDEFINED; - //legend.legendWidth = UNDEFINED; - //legend.legendHeight = UNDEFINED; - //legend.offsetWidth = UNDEFINED; - legend.itemHeight = 0; - legend.lastLineHeight = 0; - //legend.itemX = UNDEFINED; - //legend.itemY = UNDEFINED; - //legend.lastItemY = UNDEFINED; - - // Elements - //legend.group = UNDEFINED; - //legend.box = UNDEFINED; - - // run legend - legend.render(); - - // move checkboxes - addEvent(legend.chart, 'endResize', function () { legend.positionCheckboxes(); }); - -/* // expose - return { - colorizeItem: colorizeItem, - destroyItem: destroyItem, - render: render, - destroy: destroy, - getLegendWidth: getLegendWidth, - getLegendHeight: getLegendHeight - };*/ - }, - - /** - * Set the colors for the legend item - * @param {Object} item A Series or Point instance - * @param {Object} visible Dimmed or colored - */ - colorizeItem: function (item, visible) { - var legend = this, - options = legend.options, - legendItem = item.legendItem, - legendLine = item.legendLine, - legendSymbol = item.legendSymbol, - hiddenColor = legend.itemHiddenStyle.color, - textColor = visible ? options.itemStyle.color : hiddenColor, - symbolColor = visible ? item.color : hiddenColor; - - if (legendItem) { - legendItem.css({ fill: textColor }); - } - if (legendLine) { - legendLine.attr({ stroke: symbolColor }); - } - if (legendSymbol) { - legendSymbol.attr({ - stroke: symbolColor, - fill: symbolColor - }); - } - }, - - /** - * Position the legend item - * @param {Object} item A Series or Point instance - */ - positionItem: function (item) { - var legend = this, - options = legend.options, - symbolPadding = options.symbolPadding, - ltr = !options.rtl, - legendItemPos = item._legendItemPos, - itemX = legendItemPos[0], - itemY = legendItemPos[1], - checkbox = item.checkbox; - - if (item.legendGroup) { - item.legendGroup.translate( - ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4, - itemY - ); - } - - if (checkbox) { - checkbox.x = itemX; - checkbox.y = itemY; - } - }, - - /** - * Destroy a single legend item - * @param {Object} item The series or point - */ - destroyItem: function (item) { - var checkbox = item.checkbox; - - // destroy SVG elements - each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function (key) { - if (item[key]) { - item[key].destroy(); - } - }); - - if (checkbox) { - discardElement(item.checkbox); - } - }, - - /** - * Destroys the legend. - */ - destroy: function () { - var legend = this, - legendGroup = legend.group, - box = legend.box; - - if (box) { - legend.box = box.destroy(); - } - - if (legendGroup) { - legend.group = legendGroup.destroy(); - } - }, - - /** - * Position the checkboxes after the width is determined - */ - positionCheckboxes: function () { - var legend = this; - - each(legend.allItems, function (item) { - var checkbox = item.checkbox, - alignAttr = legend.group.alignAttr; - if (checkbox) { - css(checkbox, { - left: (alignAttr.translateX + item.legendItemWidth + checkbox.x - 20) + PX, - top: (alignAttr.translateY + checkbox.y + 3) + PX - }); - } - }); - }, - - /** - * Render a single specific legend item - * @param {Object} item A series or point - */ - renderItem: function (item) { - var legend = this, - chart = legend.chart, - renderer = chart.renderer, - options = legend.options, - horizontal = options.layout === 'horizontal', - symbolWidth = options.symbolWidth, - symbolPadding = options.symbolPadding, - itemStyle = legend.itemStyle, - itemHiddenStyle = legend.itemHiddenStyle, - padding = legend.padding, - ltr = !options.rtl, - itemHeight, - widthOption = options.width, - itemMarginBottom = options.itemMarginBottom || 0, - itemMarginTop = legend.itemMarginTop, - initialItemX = legend.initialItemX, - bBox, - itemWidth, - li = item.legendItem, - series = item.series || item, - itemOptions = series.options, - showCheckbox = itemOptions.showCheckbox; - - if (!li) { // generate it once, later move it - - // Generate the group box - // A group to hold the symbol and text. Text is to be appended in Legend class. - item.legendGroup = renderer.g('legend-item') - .attr({ zIndex: 1 }) - .add(legend.scrollGroup); - - // Draw the legend symbol inside the group box - series.drawLegendSymbol(legend, item); - - // Generate the list item text and add it to the group - item.legendItem = li = renderer.text( - options.labelFormatter.call(item), - ltr ? symbolWidth + symbolPadding : -symbolPadding, - legend.baseline, - options.useHTML - ) - .css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021) - .attr({ - align: ltr ? 'left' : 'right', - zIndex: 2 - }) - .add(item.legendGroup); - - // Set the events on the item group - item.legendGroup.on('mouseover', function () { - item.setState(HOVER_STATE); - li.css(legend.options.itemHoverStyle); - }) - .on('mouseout', function () { - li.css(item.visible ? itemStyle : itemHiddenStyle); - item.setState(); - }) - .on('click', function (event) { - var strLegendItemClick = 'legendItemClick', - fnLegendItemClick = function () { - item.setVisible(); - }; - - // Pass over the click/touch event. #4. - event = { - browserEvent: event - }; - - // click the name or symbol - if (item.firePointEvent) { // point - item.firePointEvent(strLegendItemClick, event, fnLegendItemClick); - } else { - fireEvent(item, strLegendItemClick, event, fnLegendItemClick); - } - }); - - // Colorize the items - legend.colorizeItem(item, item.visible); - - // add the HTML checkbox on top - if (itemOptions && showCheckbox) { - item.checkbox = createElement('input', { - type: 'checkbox', - checked: item.selected, - defaultChecked: item.selected // required by IE7 - }, options.itemCheckboxStyle, chart.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 + - (showCheckbox ? 20 : 0); - legend.itemHeight = itemHeight = bBox.height; - - // if the item exceeds the width, start a new line - if (horizontal && legend.itemX - initialItemX + itemWidth > - (widthOption || (chart.chartWidth - 2 * padding - initialItemX))) { - legend.itemX = initialItemX; - legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom; - legend.lastLineHeight = 0; // reset for next line - } - - // If the item exceeds the height, start a new column - /*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) { - legend.itemY = legend.initialItemY; - legend.itemX += legend.maxItemWidth; - legend.maxItemWidth = 0; - }*/ - - // Set the edge positions - legend.maxItemWidth = mathMax(legend.maxItemWidth, itemWidth); - legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom; - legend.lastLineHeight = mathMax(itemHeight, legend.lastLineHeight); // #915 - - // cache the position of the newly generated or reordered items - item._legendItemPos = [legend.itemX, legend.itemY]; - - // advance - if (horizontal) { - legend.itemX += itemWidth; - - } else { - legend.itemY += itemMarginTop + itemHeight + itemMarginBottom; - legend.lastLineHeight = itemHeight; - } - - // the width of the widest item - legend.offsetWidth = widthOption || mathMax( - horizontal ? legend.itemX - initialItemX : itemWidth, - legend.offsetWidth - ); - }, - - /** - * 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. - */ - render: function () { - var legend = this, - chart = legend.chart, - renderer = chart.renderer, - legendGroup = legend.group, - allItems, - display, - legendWidth, - legendHeight, - box = legend.box, - options = legend.options, - padding = legend.padding, - legendBorderWidth = options.borderWidth, - legendBackgroundColor = options.backgroundColor; - - legend.itemX = legend.initialItemX; - legend.itemY = legend.initialItemY; - legend.offsetWidth = 0; - legend.lastItemY = 0; - - if (!legendGroup) { - legend.group = legendGroup = renderer.g('legend') - // #414, #759. Trackers will be drawn above the legend, but we have - // to sacrifice that because tooltips need to be above the legend - // and trackers above tooltips - .attr({ zIndex: 7 }) - .add(); - legend.contentGroup = renderer.g() - .attr({ zIndex: 1 }) // above background - .add(legendGroup); - legend.scrollGroup = renderer.g() - .add(legend.contentGroup); - legend.clipRect = renderer.clipRect(0, 0, 9999, chart.chartHeight); - legend.contentGroup.clip(legend.clipRect); - } - - // add each series or point - allItems = []; - each(chart.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( - serie.legendItems || - (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 (options.reversed) { - allItems.reverse(); - } - - legend.allItems = allItems; - legend.display = display = !!allItems.length; - - // render the items - each(allItems, function (item) { - legend.renderItem(item); - }); - - // Draw the border - legendWidth = options.width || legend.offsetWidth; - legendHeight = legend.lastItemY + legend.lastLineHeight; - - - legendHeight = legend.handleOverflow(legendHeight); - - if (legendBorderWidth || legendBackgroundColor) { - legendWidth += padding; - legendHeight += padding; - - if (!box) { - legend.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[display ? 'show' : 'hide'](); - } - - legend.legendWidth = legendWidth; - legend.legendHeight = legendHeight; - - // Now that the legend width and height are established, put the items in the - // final position - each(allItems, function (item) { - legend.positionItem(item); - }); - - // 1.x compatibility: positioning based on style - /*var props = ['left', 'right', 'top', 'bottom'], - prop, - i = 4; - while (i--) { - prop = props[i]; - if (options.style[prop] && options.style[prop] !== 'auto') { - options[i < 2 ? 'align' : 'verticalAlign'] = prop; - options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1); - } - }*/ - - if (display) { - legendGroup.align(extend({ - width: legendWidth, - height: legendHeight - }, options), true, chart.spacingBox); - } - - if (!chart.isResizing) { - this.positionCheckboxes(); - } - }, - - /** - * Set up the overflow handling by adding navigation with up and down arrows below the - * legend. - */ - handleOverflow: function (legendHeight) { - var legend = this, - chart = this.chart, - renderer = chart.renderer, - pageCount, - options = this.options, - optionsY = options.y, - alignTop = options.verticalAlign === 'top', - spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding, - maxHeight = options.maxHeight, // docs - clipHeight, - clipRect = this.clipRect, - navOptions = options.navigation, - animation = pick(navOptions.animation, true), - arrowSize = navOptions.arrowSize || 12, - nav = this.nav; - - // Adjust the height - if (options.layout === 'horizontal') { - spaceHeight /= 2; - } - if (maxHeight) { - spaceHeight = mathMin(spaceHeight, maxHeight); - } - - // Reset the legend height and adjust the clipping rectangle - if (legendHeight > spaceHeight) { - - this.clipHeight = clipHeight = spaceHeight - 20; - this.pageCount = pageCount = mathCeil(legendHeight / clipHeight); - this.currentPage = pick(this.currentPage, 1); - this.fullHeight = legendHeight; - - clipRect.attr({ - height: clipHeight - }); - - // Add navigation elements - if (!nav) { - this.nav = nav = renderer.g().attr({ zIndex: 1 }).add(this.group); - this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize) - .on('click', function () { - legend.scroll(-1, animation); - }) - .add(nav); - this.pager = renderer.text('', 15, 10) - .css(navOptions.style) - .add(nav); - this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize) - .on('click', function () { - legend.scroll(1, animation); - }) - .add(nav); - } - - // Set initial position - legend.scroll(0); - - legendHeight = spaceHeight; - - } else if (nav) { - clipRect.attr({ - height: chart.chartHeight - }); - nav.hide(); - this.scrollGroup.attr({ - translateY: 1 - }); - } - - return legendHeight; - }, - - /** - * Scroll the legend by a number of pages - * @param {Object} scrollBy - * @param {Object} animation - */ - scroll: function (scrollBy, animation) { - var pageCount = this.pageCount, - currentPage = this.currentPage + scrollBy, - clipHeight = this.clipHeight, - navOptions = this.options.navigation, - activeColor = navOptions.activeColor, - inactiveColor = navOptions.inactiveColor, - pager = this.pager, - padding = this.padding; - - // When resizing while looking at the last page - if (currentPage > pageCount) { - currentPage = pageCount; - } - - if (currentPage > 0) { - - if (animation !== UNDEFINED) { - setAnimation(animation, this.chart); - } - - this.nav.attr({ - translateX: padding, - translateY: clipHeight + 7, - visibility: VISIBLE - }); - this.up.attr({ - fill: currentPage === 1 ? inactiveColor : activeColor - }) - .css({ - cursor: currentPage === 1 ? 'default' : 'pointer' - }); - pager.attr({ - text: currentPage + '/' + this.pageCount - }); - this.down.attr({ - x: 18 + this.pager.getBBox().width, // adjust to text width - fill: currentPage === pageCount ? inactiveColor : activeColor - }) - .css({ - cursor: currentPage === pageCount ? 'default' : 'pointer' - }); - - this.scrollGroup.animate({ - translateY: -mathMin(clipHeight * (currentPage - 1), this.fullHeight - clipHeight + padding) + 1 - }); - pager.attr({ - text: currentPage + '/' + pageCount - }); - - - this.currentPage = currentPage; - } - - } - -}; - - -/** - * The chart class - * @param {Object} options - * @param {Function} callback Function to run when the chart has loaded - */ -function Chart(userOptions, callback) { - // Handle regular options - var options, - seriesOptions = userOptions.series; // skip merging data points to increase performance - userOptions.series = null; - options = merge(defaultOptions, userOptions); // do the merge - options.series = userOptions.series = seriesOptions; // set back the series data - - var optionsChart = options.chart, - optionsMargin = optionsChart.margin, - margin = isObject(optionsMargin) ? - optionsMargin : - [optionsMargin, optionsMargin, optionsMargin, optionsMargin]; - - this.optionsMarginTop = pick(optionsChart.marginTop, margin[0]); - this.optionsMarginRight = pick(optionsChart.marginRight, margin[1]); - this.optionsMarginBottom = pick(optionsChart.marginBottom, margin[2]); - this.optionsMarginLeft = pick(optionsChart.marginLeft, margin[3]); - - var chartEvents = optionsChart.events; - - this.runChartClick = chartEvents && !!chartEvents.click; - this.callback = callback; - this.isResizing = 0; - this.options = options; - //chartTitleOptions = UNDEFINED; - //chartSubtitleOptions = UNDEFINED; - - this.axes = []; - this.series = []; - this.hasCartesianSeries = optionsChart.showAxes; - //this.axisOffset = UNDEFINED; - //this.maxTicks = UNDEFINED; // handle the greatest amount of ticks on grouped axes - //this.inverted = UNDEFINED; - //this.loadingShown = UNDEFINED; - //this.container = UNDEFINED; - //this.chartWidth = UNDEFINED; - //this.chartHeight = UNDEFINED; - //this.marginRight = UNDEFINED; - //this.marginBottom = UNDEFINED; - //this.containerWidth = UNDEFINED; - //this.containerHeight = UNDEFINED; - //this.oldChartWidth = UNDEFINED; - //this.oldChartHeight = UNDEFINED; - - //this.renderTo = UNDEFINED; - //this.renderToClone = UNDEFINED; - //this.tracker = UNDEFINED; - - //this.spacingBox = UNDEFINED - - //this.legend = UNDEFINED; - - // Elements - //this.chartBackground = UNDEFINED; - //this.plotBackground = UNDEFINED; - //this.plotBGImage = UNDEFINED; - //this.plotBorder = UNDEFINED; - //this.loadingDiv = UNDEFINED; - //this.loadingSpan = UNDEFINED; - - this.init(chartEvents); -} - -Chart.prototype = { - - /** - * Initialize an individual series, called internally before render time - */ - initSeries: function (options) { - var chart = this, - optionsChart = chart.options.chart, - type = options.type || optionsChart.type || optionsChart.defaultSeriesType, - series = new seriesTypes[type](); - - series.init(this, options); - return series; - }, - - /** - * 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 - */ - addSeries: function (options, redraw, animation) { - var series, - chart = this; - - if (options) { - setAnimation(animation, chart); - redraw = pick(redraw, true); // defaults to true - - fireEvent(chart, 'addSeries', { options: options }, function () { - chart.initSeries(options); - //series = chart.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 plot area - * @param {Number} y Pixel y relative to the plot area - */ - isInsidePlot: function (x, y) { - return x >= 0 && - x <= this.plotWidth && - y >= 0 && - y <= this.plotHeight; - }, - - /** - * Adjust all axes tick amounts - */ - adjustTickAmounts: function () { - if (this.options.chart.alignTicks !== false) { - each(this.axes, function (axis) { - axis.adjustTickAmount(); - }); - } - this.maxTicks = null; - }, - - /** - * Redraw legend, axes or series based on updated data - * - * @param {Boolean|Object} animation Whether to apply animation, and optionally animation - * configuration - */ - redraw: function (animation) { - var chart = this, - axes = chart.axes, - series = chart.series, - tracker = chart.tracker, - legend = chart.legend, - redrawLegend = chart.isDirtyLegend, - hasStackedSeries, - isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed? - seriesLength = series.length, - i = seriesLength, - clipRect = chart.clipRect, - serie, - renderer = chart.renderer, - isHiddenChart = renderer.isHidden(); - - setAnimation(animation, chart); - - if (isHiddenChart) { - chart.cloneRenderTo(); - } - - // 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 - if (serie.options.legendType === 'point') { - redrawLegend = true; - } - } - }); - - // handle added or removed series - if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed - // draw legend graphics - legend.render(); - - chart.isDirtyLegend = false; - } - - - if (chart.hasCartesianSeries) { - if (!chart.isResizing) { - - // reset maxTicks - chart.maxTicks = null; - - // set axes scales - each(axes, function (axis) { - axis.setScale(); - }); - } - chart.adjustTickAmounts(); - chart.getMargins(); - - // redraw axes - each(axes, function (axis) { - - // Fire 'afterSetExtremes' only if extremes are set - if (axis.isDirtyExtremes) { // #821 - axis.isDirtyExtremes = false; - fireEvent(axis, 'afterSetExtremes', axis.getExtremes()); // #747, #751 - } - - if (axis.isDirty || isDirtyBox || hasStackedSeries) { - axis.redraw(); - isDirtyBox = true; // #792 - } - }); - - - } - - // the plot areas size has changed - if (isDirtyBox) { - chart.drawChartBox(); - - // move clip rect - if (clipRect) { - stop(clipRect); - clipRect.animate({ // for chart resize - width: chart.plotSizeX, - height: chart.plotSizeY + 1 - }); - } - - } - - - // redraw affected series - each(series, function (serie) { - if (serie.isDirty && serie.visible && - (!serie.isCartesian || serie.xAxis)) { // issue #153 - serie.redraw(); - } - }); - - - // move tooltip or reset - if (tracker && tracker.resetTracker) { - tracker.resetTracker(true); - } - - // redraw if canvas - renderer.draw(); - - // fire the event - fireEvent(chart, 'redraw'); // jQuery breaks this when calling it from addEvent. Overwrites chart.redraw - - if (isHiddenChart) { - chart.cloneRenderTo(true); - } - }, - - - - /** - * 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 - */ - showLoading: function (str) { - var chart = this, - options = chart.options, - loadingDiv = chart.loadingDiv; - - var loadingOptions = options.loading; - - // create the layer at the first call - if (!loadingDiv) { - chart.loadingDiv = loadingDiv = createElement(DIV, { - className: PREFIX + 'loading' - }, extend(loadingOptions.style, { - left: chart.plotLeft + PX, - top: chart.plotTop + PX, - width: chart.plotWidth + PX, - height: chart.plotHeight + PX, - zIndex: 10, - display: NONE - }), chart.container); - - chart.loadingSpan = createElement( - 'span', - null, - loadingOptions.labelStyle, - loadingDiv - ); - - } - - // update text - chart.loadingSpan.innerHTML = str || options.lang.loading; - - // show it - if (!chart.loadingShown) { - css(loadingDiv, { opacity: 0, display: '' }); - animate(loadingDiv, { - opacity: loadingOptions.style.opacity - }, { - duration: loadingOptions.showDuration || 0 - }); - chart.loadingShown = true; - } - }, - - /** - * Hide the loading layer - */ - hideLoading: function () { - var options = this.options, - loadingDiv = this.loadingDiv; - - if (loadingDiv) { - animate(loadingDiv, { - opacity: 0 - }, { - duration: options.loading.hideDuration || 100, - complete: function () { - css(loadingDiv, { display: NONE }); - } - }); - } - this.loadingShown = false; - }, - - /** - * Get an axis, series or point object by id. - * @param id {String} The id as given in the configuration options - */ - get: function (id) { - var chart = this, - axes = chart.axes, - series = chart.series; - - var i, - j, - points; - - // 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++) { - points = series[i].points || []; - for (j = 0; j < points.length; j++) { - if (points[j].id === id) { - return points[j]; - } - } - } - return null; - }, - - /** - * Create the Axis instances based on the config options - */ - getAxes: function () { - var chart = this, - options = this.options; - - var xAxisOptions = options.xAxis || {}, - yAxisOptions = options.yAxis || {}, - optionsArray, - 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 - optionsArray = xAxisOptions.concat(yAxisOptions); - - each(optionsArray, function (axisOptions) { - axis = new Axis(chart, axisOptions); - }); - - chart.adjustTickAmounts(); - }, - - - /** - * Get the currently selected points from all series - */ - getSelectedPoints: function () { - var points = []; - each(this.series, function (serie) { - points = points.concat(grep(serie.points, function (point) { - return point.selected; - })); - }); - return points; - }, - - /** - * Get the currently selected series - */ - getSelectedSeries: function () { - return grep(this.series, function (serie) { - return serie.selected; - }); - }, - - /** - * Display the zoom button - */ - showResetZoom: function () { - var chart = this, - lang = defaultOptions.lang, - btnOptions = chart.options.chart.resetZoomButton, - theme = btnOptions.theme, - states = theme.states, - box = btnOptions.relativeTo === 'chart' ? null : { - x: chart.plotLeft, - y: chart.plotTop, - width: chart.plotWidth, - height: chart.plotHeight - }; - this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, function () { chart.zoomOut(); }, theme, states && states.hover) - .attr({ - align: btnOptions.position.align, - title: lang.resetZoomTitle - }) - .add() - .align(btnOptions.position, false, box); - }, - - /** - * Zoom out to 1:1 - */ - zoomOut: function () { - var chart = this, - resetZoomButton = chart.resetZoomButton; - - fireEvent(chart, 'selection', { resetSelection: true }, function () { chart.zoom(); }); - if (resetZoomButton) { - chart.resetZoomButton = resetZoomButton.destroy(); - } - }, - - /** - * Zoom into a given portion of the chart given by axis coordinates - * @param {Object} event - */ - zoom: function (event) { - var chart = this, - optionsChart = chart.options.chart; - - // add button to reset selection - var hasZoomed; - - if (chart.resetZoomEnabled !== false && !chart.resetZoomButton) { // hook for Stock charts etc. - chart.showResetZoom(); - } - - // if zoom is called with no arguments, reset the axes - if (!event || event.resetSelection) { - each(chart.axes, function (axis) { - if (axis.options.zoomEnabled !== false) { - axis.setExtremes(null, null, false); - hasZoomed = true; - } - }); - } else { // else, zoom in on all axes - each(event.xAxis.concat(event.yAxis), function (axisData) { - var axis = axisData.axis; - - // don't zoom more than minRange - if (chart.tracker[axis.isXAxis ? 'zoomX' : 'zoomY']) { - axis.setExtremes(axisData.min, axisData.max, false); - hasZoomed = true; - } - }); - } - - // Redraw - if (hasZoomed) { - chart.redraw( - pick(optionsChart.animation, chart.pointCount < 100) // animation - ); - } - }, - - /** - * Pan the chart by dragging the mouse across the pane. This function is called - * on mouse move, and the distance to pan is computed from chartX compared to - * the first chartX position in the dragging operation. - */ - pan: function (chartX) { - var chart = this; - - var xAxis = chart.xAxis[0], - mouseDownX = chart.mouseDownX, - halfPointRange = xAxis.pointRange / 2, - extremes = xAxis.getExtremes(), - newMin = xAxis.translate(mouseDownX - chartX, true) + halfPointRange, - newMax = xAxis.translate(mouseDownX + chart.plotWidth - chartX, true) - halfPointRange, - hoverPoints = chart.hoverPoints; - - // remove active points for shared tooltip - if (hoverPoints) { - each(hoverPoints, function (point) { - point.setState(); - }); - } - - if (xAxis.series.length && newMin > mathMin(extremes.dataMin, extremes.min) && newMax < mathMax(extremes.dataMax, extremes.max)) { - xAxis.setExtremes(newMin, newMax, true, false); - } - - chart.mouseDownX = chartX; // set new reference for next run - css(chart.container, { cursor: 'move' }); - }, - - /** - * Show the title and subtitle of the chart - * - * @param titleOptions {Object} New title options - * @param subtitleOptions {Object} New subtitle options - * - */ - setTitle: function (titleOptions, subtitleOptions) { - var chart = this, - options = chart.options, - chartTitleOptions, - chartSubtitleOptions; - - chart.chartTitleOptions = chartTitleOptions = merge(options.title, titleOptions); - chart.chartSubtitleOptions = 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] = chart.renderer.text( - chartTitleOptions.text, - 0, - 0, - chartTitleOptions.useHTML - ) - .attr({ - align: chartTitleOptions.align, - 'class': PREFIX + name, - zIndex: chartTitleOptions.zIndex || 4 - }) - .css(chartTitleOptions.style) - .add() - .align(chartTitleOptions, false, chart.spacingBox); - } - }); - - }, - - /** - * Get chart width and height according to options and container size - */ - getChartSize: function () { - var chart = this, - optionsChart = chart.options.chart, - renderTo = chart.renderToClone || chart.renderTo; - - // get inner width and height from jQuery (#824) - chart.containerWidth = adapterRun(renderTo, 'width'); - chart.containerHeight = adapterRun(renderTo, 'height'); - - chart.chartWidth = optionsChart.width || chart.containerWidth || 600; - chart.chartHeight = optionsChart.height || - // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7: - (chart.containerHeight > 19 ? chart.containerHeight : 400); - }, - - /** - * Create a clone of the chart's renderTo div and place it outside the viewport to allow - * size computation on chart.render and chart.redraw - */ - cloneRenderTo: function (revert) { - var clone = this.renderToClone, - container = this.container; - - // Destroy the clone and bring the container back to the real renderTo div - if (revert) { - if (clone) { - this.renderTo.appendChild(container); - discardElement(clone); - delete this.renderToClone; - } - - // Set up the clone - } else { - if (container) { - this.renderTo.removeChild(container); // do not clone this - } - this.renderToClone = clone = this.renderTo.cloneNode(0); - css(clone, { - position: ABSOLUTE, - top: '-9999px', - display: 'block' // #833 - }); - doc.body.appendChild(clone); - if (container) { - clone.appendChild(container); - } - } - }, - - /** - * Get the containing element, determine the size and create the inner container - * div to hold the chart - */ - getContainer: function () { - var chart = this, - container, - optionsChart = chart.options.chart, - chartWidth, - chartHeight, - renderTo, - containerId; - - chart.renderTo = renderTo = optionsChart.renderTo; - containerId = PREFIX + idCounter++; - - if (isString(renderTo)) { - chart.renderTo = renderTo = doc.getElementById(renderTo); - } - - // Display an error if the renderTo is wrong - if (!renderTo) { - error(13, true); - } - - // 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) { - chart.cloneRenderTo(); - } - - // get the width and height - chart.getChartSize(); - chartWidth = chart.chartWidth; - chartHeight = chart.chartHeight; - - // create the inner container - chart.container = container = createElement(DIV, { - className: PREFIX + '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', - lineHeight: 'normal' // #427 - }, optionsChart.style), - chart.renderToClone || renderTo - ); - - chart.renderer = - optionsChart.forExport ? // force SVG, used for SVG export - new SVGRenderer(container, chartWidth, chartHeight, true) : - new Renderer(container, chartWidth, chartHeight); - - if (useCanVG) { - // If we need canvg library, extend and configure the renderer - // to get the tracker for translating mouse events - chart.renderer.create(chart, container, chartWidth, chartHeight); - } - }, - - /** - * 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 chart = this, - optionsChart = chart.options.chart, - spacingTop = optionsChart.spacingTop, - spacingRight = optionsChart.spacingRight, - spacingBottom = optionsChart.spacingBottom, - spacingLeft = optionsChart.spacingLeft, - axisOffset, - legend = chart.legend, - optionsMarginTop = chart.optionsMarginTop, - optionsMarginLeft = chart.optionsMarginLeft, - optionsMarginRight = chart.optionsMarginRight, - optionsMarginBottom = chart.optionsMarginBottom, - chartTitleOptions = chart.chartTitleOptions, - chartSubtitleOptions = chart.chartSubtitleOptions, - legendOptions = chart.options.legend, - legendMargin = pick(legendOptions.margin, 10), - legendX = legendOptions.x, - legendY = legendOptions.y, - align = legendOptions.align, - verticalAlign = legendOptions.verticalAlign, - titleOffset; - - chart.resetMargins(); - axisOffset = chart.axisOffset; - - // adjust for title and subtitle - if ((chart.title || chart.subtitle) && !defined(chart.optionsMarginTop)) { - titleOffset = mathMax( - (chart.title && !chartTitleOptions.floating && !chartTitleOptions.verticalAlign && chartTitleOptions.y) || 0, - (chart.subtitle && !chartSubtitleOptions.floating && !chartSubtitleOptions.verticalAlign && chartSubtitleOptions.y) || 0 - ); - if (titleOffset) { - chart.plotTop = mathMax(chart.plotTop, titleOffset + pick(chartTitleOptions.margin, 15) + spacingTop); - } - } - // adjust for legend - if (legend.display && !legendOptions.floating) { - if (align === 'right') { // horizontal alignment handled first - if (!defined(optionsMarginRight)) { - chart.marginRight = mathMax( - chart.marginRight, - legend.legendWidth - legendX + legendMargin + spacingRight - ); - } - } else if (align === 'left') { - if (!defined(optionsMarginLeft)) { - chart.plotLeft = mathMax( - chart.plotLeft, - legend.legendWidth + legendX + legendMargin + spacingLeft - ); - } - - } else if (verticalAlign === 'top') { - if (!defined(optionsMarginTop)) { - chart.plotTop = mathMax( - chart.plotTop, - legend.legendHeight + legendY + legendMargin + spacingTop - ); - } - - } else if (verticalAlign === 'bottom') { - if (!defined(optionsMarginBottom)) { - chart.marginBottom = mathMax( - chart.marginBottom, - legend.legendHeight - legendY + legendMargin + spacingBottom - ); - } - } - } - - // adjust for scroller - if (chart.extraBottomMargin) { - chart.marginBottom += chart.extraBottomMargin; - } - if (chart.extraTopMargin) { - chart.plotTop += chart.extraTopMargin; - } - - // pre-render axes to get labels offset width - if (chart.hasCartesianSeries) { - each(chart.axes, function (axis) { - axis.getOffset(); - }); - } - - if (!defined(optionsMarginLeft)) { - chart.plotLeft += axisOffset[3]; - } - if (!defined(optionsMarginTop)) { - chart.plotTop += axisOffset[0]; - } - if (!defined(optionsMarginBottom)) { - chart.marginBottom += axisOffset[2]; - } - if (!defined(optionsMarginRight)) { - chart.marginRight += axisOffset[1]; - } - - chart.setChartSize(); - - }, - - /** - * Add the event handlers necessary for auto resizing - * - */ - initReflow: function () { - var chart = this, - optionsChart = chart.options.chart, - renderTo = chart.renderTo; - - var reflowTimeout; - function reflow(e) { - var width = optionsChart.width || adapterRun(renderTo, 'width'), - height = optionsChart.height || adapterRun(renderTo, 'height'), - target = e ? e.target : win; // #805 - MooTools doesn't supply e - - // Width and height checks for display:none. Target is doc in IE8 and Opera, - // win in Firefox, Chrome and IE9. - if (width && height && (target === win || target === doc)) { - - if (width !== chart.containerWidth || height !== chart.containerHeight) { - clearTimeout(reflowTimeout); - reflowTimeout = setTimeout(function () { - chart.resize(width, height, false); - }, 100); - } - chart.containerWidth = width; - chart.containerHeight = height; - } - } - addEvent(win, 'resize', reflow); - addEvent(chart, 'destroy', function () { - removeEvent(win, 'resize', reflow); - }); - }, - - /** - * Fires endResize event on chart instance. - */ - fireEndResize: function () { - var chart = this; - - if (chart) { - fireEvent(chart, 'endResize', null, function () { - chart.isResizing -= 1; - }); - } - }, - - /** - * Resize the chart to a given width and height - * @param {Number} width - * @param {Number} height - * @param {Object|Boolean} animation - */ - // TODO: This method is called setSize in the api - resize: function (width, height, animation) { - var chart = this, - chartWidth, - chartHeight, - spacingBox, - chartTitle = chart.title, - chartSubtitle = chart.subtitle; - - chart.isResizing += 1; - - // set the animation for the current process - setAnimation(animation, chart); - - chart.oldChartHeight = chart.chartHeight; - chart.oldChartWidth = chart.chartWidth; - if (defined(width)) { - chart.chartWidth = chartWidth = mathRound(width); - } - if (defined(height)) { - chart.chartHeight = chartHeight = mathRound(height); - } - - css(chart.container, { - width: chartWidth + PX, - height: chartHeight + PX - }); - chart.renderer.setSize(chartWidth, chartHeight, animation); - - // update axis lengths for more correct tick intervals: - chart.plotWidth = chartWidth - chart.plotLeft - chart.marginRight; - chart.plotHeight = chartHeight - chart.plotTop - chart.marginBottom; - - // handle axes - chart.maxTicks = null; - each(chart.axes, function (axis) { - axis.isDirty = true; - axis.setScale(); - }); - - // make sure non-cartesian series are also handled - each(chart.series, function (serie) { - serie.isDirty = true; - }); - - chart.isDirtyLegend = true; // force legend redraw - chart.isDirtyBox = true; // force redraw of plot and chart border - - chart.getMargins(); - - // move titles - spacingBox = chart.spacingBox; - if (chartTitle) { - chartTitle.align(null, null, spacingBox); - } - if (chartSubtitle) { - chartSubtitle.align(null, null, spacingBox); - } - - chart.redraw(animation); - - - chart.oldChartHeight = null; - fireEvent(chart, 'resize'); - - // fire endResize and set isResizing back - // If animation is disabled, fire without delay - if (globalAnimation === false) { - chart.fireEndResize(); - } else { // else set a timeout with the animation duration - setTimeout(chart.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 () { - var chart = this, - inverted = chart.inverted, - chartWidth = chart.chartWidth, - chartHeight = chart.chartHeight, - optionsChart = chart.options.chart, - spacingTop = optionsChart.spacingTop, - spacingRight = optionsChart.spacingRight, - spacingBottom = optionsChart.spacingBottom, - spacingLeft = optionsChart.spacingLeft; - - chart.plotLeft = mathRound(chart.plotLeft); - chart.plotTop = mathRound(chart.plotTop); - chart.plotWidth = mathRound(chartWidth - chart.plotLeft - chart.marginRight); - chart.plotHeight = mathRound(chartHeight - chart.plotTop - chart.marginBottom); - - chart.plotSizeX = inverted ? chart.plotHeight : chart.plotWidth; - chart.plotSizeY = inverted ? chart.plotWidth : chart.plotHeight; - - chart.spacingBox = { - x: spacingLeft, - y: spacingTop, - width: chartWidth - spacingLeft - spacingRight, - height: chartHeight - spacingTop - spacingBottom - }; - - each(chart.axes, function (axis) { - axis.setAxisSize(); - axis.setAxisTranslation(); - }); - }, - - /** - * Initial margins before auto size margins are applied - */ - resetMargins: function () { - var chart = this, - optionsChart = chart.options.chart, - spacingTop = optionsChart.spacingTop, - spacingRight = optionsChart.spacingRight, - spacingBottom = optionsChart.spacingBottom, - spacingLeft = optionsChart.spacingLeft; - - chart.plotTop = pick(chart.optionsMarginTop, spacingTop); - chart.marginRight = pick(chart.optionsMarginRight, spacingRight); - chart.marginBottom = pick(chart.optionsMarginBottom, spacingBottom); - chart.plotLeft = pick(chart.optionsMarginLeft, spacingLeft); - chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left - }, - - /** - * Draw the borders and backgrounds for chart and plot area - */ - drawChartBox: function () { - var chart = this, - optionsChart = chart.options.chart, - renderer = chart.renderer, - chartWidth = chart.chartWidth, - chartHeight = chart.chartHeight, - chartBackground = chart.chartBackground, - plotBackground = chart.plotBackground, - plotBorder = chart.plotBorder, - plotBGImage = chart.plotBGImage, - chartBorderWidth = optionsChart.borderWidth || 0, - chartBackgroundColor = optionsChart.backgroundColor, - plotBackgroundColor = optionsChart.plotBackgroundColor, - plotBackgroundImage = optionsChart.plotBackgroundImage, - mgn, - bgAttr, - plotSize = { - x: chart.plotLeft, - y: chart.plotTop, - width: chart.plotWidth, - height: chart.plotHeight - }; - - // Chart area - mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0); - - if (chartBorderWidth || chartBackgroundColor) { - if (!chartBackground) { - - bgAttr = { - fill: chartBackgroundColor || NONE - }; - if (chartBorderWidth) { // #980 - bgAttr.stroke = optionsChart.borderColor; - bgAttr['stroke-width'] = chartBorderWidth; - } - chart.chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn, - optionsChart.borderRadius, chartBorderWidth) - .attr(bgAttr) - .add() - .shadow(optionsChart.shadow); - } else { // resize - chartBackground.animate( - chartBackground.crisp(null, null, null, chartWidth - mgn, chartHeight - mgn) - ); - } - } - - - // Plot background - if (plotBackgroundColor) { - if (!plotBackground) { - chart.plotBackground = renderer.rect(chart.plotLeft, chart.plotTop, chart.plotWidth, chart.plotHeight, 0) - .attr({ - fill: plotBackgroundColor - }) - .add() - .shadow(optionsChart.plotShadow); - } else { - plotBackground.animate(plotSize); - } - } - if (plotBackgroundImage) { - if (!plotBGImage) { - chart.plotBGImage = renderer.image(plotBackgroundImage, chart.plotLeft, chart.plotTop, chart.plotWidth, chart.plotHeight) - .add(); - } else { - plotBGImage.animate(plotSize); - } - } - - // Plot area border - if (optionsChart.plotBorderWidth) { - if (!plotBorder) { - chart.plotBorder = renderer.rect(chart.plotLeft, chart.plotTop, chart.plotWidth, chart.plotHeight, 0, optionsChart.plotBorderWidth) - .attr({ - stroke: optionsChart.plotBorderColor, - 'stroke-width': optionsChart.plotBorderWidth, - zIndex: 4 - }) - .add(); - } else { - plotBorder.animate( - plotBorder.crisp(null, chart.plotLeft, chart.plotTop, chart.plotWidth, chart.plotHeight) - ); - } - } - - // reset - chart.isDirtyBox = false; - }, - - /** - * Detect whether a certain chart property is needed based on inspecting its options - * and series. This mainly applies to the chart.invert property, and in extensions to - * the chart.angular and chart.polar properties. - */ - propFromSeries: function () { - var chart = this, - optionsChart = chart.options.chart, - klass, - seriesOptions = chart.options.series, - i, - value; - - - each(['inverted', 'angular', 'polar'], function (key) { - - // The default series type's class - klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType]; - - // Get the value from available chart-wide properties - value = ( - chart[key] || // 1. it is set before - optionsChart[key] || // 2. it is set in the options - (klass && klass.prototype[key]) // 3. it's default series class requires it - ); - - // 4. Check if any the chart's series require it - i = seriesOptions && seriesOptions.length; - while (!value && i--) { - klass = seriesTypes[seriesOptions[i].type]; - if (klass && klass.prototype[key]) { - value = true; - } - } - - // Set the chart property - chart[key] = value; - }); - - }, - - /** - * Render all graphics for the chart - */ - render: function () { - var chart = this, - axes = chart.axes, - renderer = chart.renderer, - options = chart.options; - - var labels = options.labels, - credits = options.credits, - creditsHref; - - // Title - chart.setTitle(); - - - // Legend - chart.legend = new Legend(chart); - - // Get margins by pre-rendering axes - // set axes scales - each(axes, function (axis) { - axis.setScale(); - }); - chart.getMargins(); - - chart.maxTicks = null; // reset for second pass - each(axes, function (axis) { - axis.setTickPositions(true); // update to reflect the new margins - axis.setMaxTicks(); - }); - chart.adjustTickAmounts(); - chart.getMargins(); // second pass to check for new labels - - - // Draw the borders and backgrounds - chart.drawChartBox(); - - - // Axes - if (chart.hasCartesianSeries) { - each(axes, function (axis) { - axis.render(); - }); - } - - // The series - if (!chart.seriesGroup) { - chart.seriesGroup = renderer.g('series-group') - .attr({ zIndex: 3 }) - .add(); - } - each(chart.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) + chart.plotLeft, - y = pInt(style.top) + chart.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(); - - }); - } - - // 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); - } - - // Set flag - chart.hasRendered = true; - - }, - - /** - * Clean up memory usage - */ - destroy: function () { - var chart = this, - axes = chart.axes, - series = chart.series, - container = chart.container; - - 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(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', 'chartBackground', 'plotBackground', 'plotBGImage', 'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'tracker', 'scroller', 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'], function (name) { - var prop = chart[name]; - - if (prop) { - chart[name] = prop.destroy(); - } - }); - - // 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; - } - - // clean it all up - for (i in chart) { - delete chart[i]; - } - - chart.options = null; - chart = null; - }, - - /** - * Prepare for first rendering after all data are loaded - */ - firstRender: function () { - var chart = this, - options = chart.options, - callback = chart.callback; - - // 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)) || (useCanVG && !win.canvg)) { - /*jslint eqeq: false*/ - if (useCanVG) { - // Delay rendering until canvg library is downloaded and ready - CanVGController.push(function () { chart.firstRender(); }, options.global.canvasToolsURL); - } else { - doc.attachEvent(ONREADYSTATECHANGE, function () { - doc.detachEvent(ONREADYSTATECHANGE, chart.firstRender); - if (doc.readyState === COMPLETE) { - chart.firstRender(); - } - }); - } - return; - } - - // create the container - chart.getContainer(); - - // Run an early event after the container and renderer are established - fireEvent(chart, 'init'); - - // Initialize range selector for stock charts - if (Highcharts.RangeSelector && options.rangeSelector.enabled) { - chart.rangeSelector = new Highcharts.RangeSelector(chart); - } - - chart.resetMargins(); - chart.setChartSize(); - - // Set the common chart properties (mainly invert) from the given series - chart.propFromSeries(); - - // get axes - chart.getAxes(); - - // Initialize the series - each(options.series || [], function (serieOptions) { - chart.initSeries(serieOptions); - }); - - // Run an event where series and axes can be added - //fireEvent(chart, 'beforeRender'); - - // Initialize scroller for stock charts - if (Highcharts.Scroller && (options.navigator.enabled || options.scrollbar.enabled)) { - chart.scroller = new Highcharts.Scroller(chart); - } - - // depends on inverted and on margins being set - chart.tracker = new MouseTracker(chart, options); - - chart.render(); - - // add canvas - chart.renderer.draw(); - // run callbacks - if (callback) { - callback.apply(chart, [chart]); - } - each(chart.callbacks, function (fn) { - fn.apply(chart, [chart]); - }); - - - // If the chart was rendered outside the top container, put it back in - chart.cloneRenderTo(true); - - fireEvent(chart, 'load'); - - }, - - init: function (chartEvents) { - var chart = this, - optionsChart = chart.options.chart, - eventType; - - // Run chart - - // Set up auto resize - if (optionsChart.reflow !== false) { - addEvent(chart, 'load', chart.initReflow); - } - - // Chart event handlers - if (chartEvents) { - for (eventType in chartEvents) { - addEvent(chart, eventType, chartEvents[eventType]); - } - } - - chart.xAxis = []; - chart.yAxis = []; - - // Expose methods and variables - chart.animation = useCanVG ? false : pick(optionsChart.animation, true); - chart.setSize = chart.resize; - 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); - }); - } - }) - */ - - chart.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, x) { - var point = this, - counters = series.chart.counters, - defaultColors; - point.series = series; - point.applyOptions(options, x); - 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, x) { - var point = this, - series = point.series, - optionsType = typeof options; - - point.config = options; - - // onedimensional array input - if (optionsType === 'number' || options === null) { - point.y = options; - } else if (typeof options[0] === 'number') { // two-dimentional array - point.x = options[0]; - point.y = options[1]; - } else if (optionsType === 'object' && typeof options.length !== 'number') { // object input - // copy options directly to point - extend(point, options); - point.options = options; - - // This is the fastest way to detect if there are individual point dataLabels that need - // to be considered in drawDataLabels. These can only occur in object configs. - if (options.dataLabels) { - series._hasPointLabels = true; - } - } else if (typeof options[0] === 'string') { // categorized data with name in first position - point.name = 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 - */ - // todo: skip this? It is only used in applyOptions, in translate it should not be used - if (point.x === UNDEFINED) { - point.x = x === UNDEFINED ? series.autoIncrement() : x; - } - - - - }, - - /** - * Destroy a point to clear memory. Its reference still stays in series.data. - */ - destroy: function () { - var point = this, - series = point.series, - chart = series.chart, - hoverPoints = chart.hoverPoints, - prop; - - chart.pointCount--; - - if (hoverPoints) { - point.setState(); - erase(hoverPoints, point); - if (!hoverPoints.length) { - chart.hoverPoints = null; - } - - } - if (point === chart.hoverPoint) { - point.onMouseOut(); - } - - // remove all events - if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive - removeEvent(point); - point.destroyElements(); - } - - if (point.legendItem) { // pies have legend items - chart.legend.destroyItem(point); - } - - for (prop in point) { - point[prop] = null; - } - - - }, - - /** - * Destroy SVG elements associated with the point - */ - destroyElements: function () { - var point = this, - props = ['graphic', 'tracker', 'dataLabel', 'group', 'connector', 'shadowGroup'], - prop, - i = 6; - while (i--) { - prop = props[i]; - if (point[prop]) { - point[prop] = point[prop].destroy(); - } - } - }, - - /** - * Return the configuration hash needed for the data label and tooltip formatters - */ - getLabelConfig: function () { - var point = this; - return { - x: point.category, - y: point.y, - key: point.name || point.category, - 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, - series = point.series, - chart = 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 || series.noSharedTooltip)) { - 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 - * - * @return {String} A string to be concatenated in to the common tooltip text - */ - tooltipFormatter: function (pointFormat) { - var point = this, - series = point.series, - seriesTooltipOptions = series.tooltipOptions, - match = pointFormat.match(/\{(series|point)\.[a-zA-Z]+\}/g), - splitter = /[{\.}]/, - obj, - key, - replacement, - repOptionKey, - parts, - prop, - i, - cfg = { // docs: percentageDecimals, percentagePrefix, percentageSuffix, totalDecimals, totalPrefix, totalSuffix - y: 0, // 0: use 'value' for repOptionKey - open: 0, - high: 0, - low: 0, - close: 0, - percentage: 1, // 1: use the self name for repOptionKey - total: 1 - }; - - // Backwards compatibility to y naming in early Highstock - seriesTooltipOptions.valuePrefix = seriesTooltipOptions.valuePrefix || seriesTooltipOptions.yPrefix; - seriesTooltipOptions.valueDecimals = seriesTooltipOptions.valueDecimals || seriesTooltipOptions.yDecimals; - seriesTooltipOptions.valueSuffix = seriesTooltipOptions.valueSuffix || seriesTooltipOptions.ySuffix; - - // loop over the variables defined on the form {series.name}, {point.y} etc - for (i in match) { - key = match[i]; - if (isString(key) && key !== pointFormat) { // IE matches more than just the variables - - // Split it further into parts - parts = (' ' + key).split(splitter); // add empty string because IE and the rest handles it differently - obj = { 'point': point, 'series': series }[parts[1]]; - prop = parts[2]; - - // Add some preformatting - if (obj === point && cfg.hasOwnProperty(prop)) { - repOptionKey = cfg[prop] ? prop : 'value'; - replacement = (seriesTooltipOptions[repOptionKey + 'Prefix'] || '') + - numberFormat(point[prop], pick(seriesTooltipOptions[repOptionKey + 'Decimals'], -1)) + - (seriesTooltipOptions[repOptionKey + 'Suffix'] || ''); - - // Automatic replacement - } else { - replacement = obj[prop]; - } - - pointFormat = pointFormat.replace(key, replacement); - } - } - - return pointFormat; - }, - - /** - * 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, - i, - data = series.data, - dataLength = data.length, - 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]); - } - } - - // record changes in the parallel arrays - for (i = 0; i < dataLength; i++) { - if (data[i] === point) { - series.xData[i] = point.x; - series.yData[i] = point.y; - series.options.data[i] = options; - break; - } - } - - // redraw - series.isDirty = true; - series.isDirtyData = 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, - i, - data = series.data, - dataLength = data.length; - - setAnimation(animation, chart); - redraw = pick(redraw, true); - - // fire the event with a default handler of removing the point - point.firePointEvent('remove', null, function () { - - //erase(series.data, point); - - for (i = 0; i < dataLength; i++) { - if (data[i] === point) { - - // splice all the parallel arrays - data.splice(i, 1); - series.options.data.splice(i, 1); - series.xData.splice(i, 1); - series.yData.splice(i, 1); - break; - } - } - - point.destroy(); - - - // redraw - series.isDirty = true; - series.isDirtyData = 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, - plotX = point.plotX, - plotY = point.plotY, - 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, - radius, - 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) { - radius = markerOptions && point.graphic.symbolName && pointAttr[state].r; - point.graphic.attr(merge( - pointAttr[state], - radius ? { // new symbol attributes (#507, #612) - x: plotX - radius, - y: plotY - radius, - width: 2 * radius, - height: 2 * radius - } : {} - )); - } else { - // if a graphic is not applied to each point in the normal state, create a shared - // graphic for the hover state - if (state && markerStateOptions) { - if (!stateMarkerGraphic) { - radius = markerStateOptions.radius; - series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol( - series.symbol, - -radius, - -radius, - 2 * radius, - 2 * radius - ) - .attr(pointAttr[state]) - .add(series.group); - } - - stateMarkerGraphic.translate( - plotX, - plotY - ); - } - - if (stateMarkerGraphic) { - stateMarkerGraphic[state ? 'show' : 'hide'](); - } - } - - point.state = state; - } -}; - -/** - * @classDescription The base function which all other series types inherit from. The data in the series is stored - * in various arrays. - * - * - First, series.options.data contains all the original config options for - * each point whether added by options or methods like series.addPoint. - * - Next, series.data contains those values converted to points, but in case the series data length - * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It - * only contains the points that have been created on demand. - * - Then there's series.points that contains all currently visible point objects. In case of cropping, - * the cropped-away points are not part of this array. The series.points array starts at series.cropStart - * compared to series.data and series.options.data. If however the series data is grouped, these can't - * be correlated one to one. - * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points. - * - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points. - * - * @param {Object} chart - * @param {Object} options - */ -var Series = function () {}; - -Series.prototype = { - - isCartesian: true, - type: 'line', - pointClass: Point, - sorted: true, // requires the data to be sorted - 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; - series.options = options = series.setOptions(options); // merge with plotOptions - - // bind the axes - series.bindAxes(); - - // set some variables - extend(series, { - index: index, - name: options.name || 'Series ' + (index + 1), - state: NORMAL_STATE, - pointAttr: {}, - visible: options.visible !== false, // true by default - selected: options.selected === true // false by default - }); - - // special - if (useCanVG) { - options.animation = false; - } - - // 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); - - // Mark cartesian - if (series.isCartesian) { - chart.hasCartesianSeries = true; - } - - // Register it in the chart - chart.series.push(series); - }, - - /** - * Set the xAxis and yAxis properties of cartesian series, and register the series - * in the axis.series array - */ - bindAxes: function () { - var series = this, - seriesOptions = series.options, - chart = series.chart, - axisOptions; - - if (series.isCartesian) { - - each(['xAxis', 'yAxis'], function (AXIS) { // repeat for xAxis and yAxis - - each(chart[AXIS], function (axis) { // loop through the chart's axis objects - - axisOptions = axis.options; - - // apply if the series xAxis or yAxis option mathches the number of the - // axis, or if undefined, use the first axis - if ((seriesOptions[AXIS] === axisOptions.index) || - (seriesOptions[AXIS] === UNDEFINED && axisOptions.index === 0)) { - - // register this series in the axis.series lookup - axis.series.push(series); - - // set this series.xAxis or series.yAxis reference - series[AXIS] = axis; - - // mark dirty for redraw - axis.isDirty = true; - } - }); - - }); - } - }, - - - /** - * 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; - }, - - /** - * Divide the series data into segments divided by null values. - */ - getSegments: function () { - var series = this, - lastNull = -1, - segments = [], - i, - points = series.points, - pointsLength = points.length; - - if (pointsLength) { // no action required for [] - - // if connect nulls, just remove null points - if (series.options.connectNulls) { - i = pointsLength; - while (i--) { - if (points[i].y === null) { - points.splice(i, 1); - } - } - if (points.length) { - segments = [points]; - } - - // else, split on null points - } else { - each(points, function (point, i) { - if (point.y === null) { - if (i > lastNull + 1) { - segments.push(points.slice(lastNull + 1, i)); - } - lastNull = i; - } else if (i === pointsLength - 1) { // last value - segments.push(points.slice(lastNull + 1, i + 1)); - } - }); - } - } - - // register it - series.segments = segments; - }, - /** - * Set the series options by merging from the options tree - * @param {Object} itemOptions - */ - setOptions: function (itemOptions) { - var series = this, - chart = series.chart, - chartOptions = chart.options, - plotOptions = chartOptions.plotOptions, - data = itemOptions.data, - options; - - itemOptions.data = null; // remove from merge to prevent looping over the data set - - options = merge( - plotOptions[this.type], - plotOptions.series, - itemOptions - ); - - // Re-insert the data array to the options and the original config (#717) - options.data = itemOptions.data = data; - - // the tooltip options are merged between global and series specific options - series.tooltipOptions = merge(chartOptions.tooltip, options.tooltip); - - return options; - - }, - /** - * Get the series' color - */ - getColor: function () { - var options = this.options, - defaultColors = this.chart.options.colors, - counters = this.chart.counters; - this.color = options.color || - (!options.colorByPoint && defaultColors[counters.color++]) || 'gray'; - counters.wrapColor(defaultColors.length); - }, - /** - * Get the series' symbol - */ - getSymbol: function () { - var series = this, - seriesMarkerOption = series.options.marker, - chart = series.chart, - defaultSymbols = chart.options.symbols, - counters = chart.counters; - series.symbol = seriesMarkerOption.symbol || defaultSymbols[counters.symbol++]; - - // don't substract radius in image symbols (#604) - if (/^url/.test(series.symbol)) { - seriesMarkerOption.radius = 0; - } - counters.wrapSymbol(defaultSymbols.length); - }, - - /** - * Get the series' symbol in the legend. This method should be overridable to create custom - * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols. - * - * @param {Object} legend The legend object - */ - drawLegendSymbol: function (legend) { - - var options = this.options, - markerOptions = options.marker, - radius, - legendOptions = legend.options, - legendSymbol, - symbolWidth = legendOptions.symbolWidth, - renderer = this.chart.renderer, - legendItemGroup = this.legendGroup, - baseline = legend.baseline, - attr; - - // Draw the line - if (options.lineWidth) { - attr = { - 'stroke-width': options.lineWidth - }; - if (options.dashStyle) { - attr.dashstyle = options.dashStyle; - } - this.legendLine = renderer.path([ - M, - 0, - baseline - 4, - L, - symbolWidth, - baseline - 4 - ]) - .attr(attr) - .add(legendItemGroup); - } - - // Draw the marker - if (markerOptions && markerOptions.enabled) { - radius = markerOptions.radius; - this.legendSymbol = legendSymbol = renderer.symbol( - this.symbol, - (symbolWidth / 2) - radius, - baseline - 4 - radius, - 2 * radius, - 2 * radius - ) - .attr(this.pointAttr[NORMAL_STATE]) - .add(legendItemGroup); - } - }, - - /** - * 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, - xData = series.xData, - yData = series.yData, - currentShift = (graph && graph.shift) || 0, - dataOptions = series.options.data, - point; - //point = (new series.pointClass()).init(series, options); - - setAnimation(animation, chart); - - // Make graph animate sideways - if (graph && shift) { - graph.shift = currentShift + 1; - } - if (area) { - if (shift) { // #780 - area.shift = currentShift + 1; - } - area.isArea = true; // needed in animation, both with and without shift - } - - // Optional redraw, defaults to true - redraw = pick(redraw, true); - - // Get options and push the point to xData, yData and series.options. In series.generatePoints - // the Point instance will be created on demand and pushed to the series.data array. - point = { series: series }; - series.pointClass.prototype.applyOptions.apply(point, [options]); - xData.push(point.x); - yData.push(series.valueCount === 4 ? [point.open, point.high, point.low, point.close] : point.y); - dataOptions.push(options); - - - // Shift the first point off the parallel arrays - // todo: consider series.removePoint(i) method - if (shift) { - if (data[0] && data[0].remove) { - data[0].remove(false); - } else { - data.shift(); - xData.shift(); - yData.shift(); - dataOptions.shift(); - } - } - series.getAttribs(); - - // redraw - series.isDirty = true; - series.isDirtyData = 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.points, - options = series.options, - initialColor = series.initialColor, - chart = series.chart, - firstPoint = null, - xAxis = series.xAxis, - i, - pointProto = series.pointClass.prototype; - - // reset properties - series.xIncrement = null; - series.pointRange = (xAxis && xAxis.categories && 1) || options.pointRange; - - if (defined(initialColor)) { // reset colors for pie - chart.counters.color = initialColor; - } - - // parallel arrays - var xData = [], - yData = [], - dataLength = data ? data.length : [], - turboThreshold = options.turboThreshold || 1000, - pt, - valueCount = series.valueCount; - - // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The - // first value is tested, and we assume that all the rest are defined the same - // way. Although the 'for' loops are similar, they are repeated inside each - // if-else conditional for max performance. - if (dataLength > turboThreshold) { - - // find the first non-null point - i = 0; - while (firstPoint === null && i < dataLength) { - firstPoint = data[i]; - i++; - } - - - if (isNumber(firstPoint)) { // assume all points are numbers - var x = pick(options.pointStart, 0), - pointInterval = pick(options.pointInterval, 1); - - for (i = 0; i < dataLength; i++) { - xData[i] = x; - yData[i] = data[i]; - x += pointInterval; - } - series.xIncrement = x; - } else if (isArray(firstPoint)) { // assume all points are arrays - if (valueCount) { // [x, low, high] or [x, o, h, l, c] - for (i = 0; i < dataLength; i++) { - pt = data[i]; - xData[i] = pt[0]; - yData[i] = pt.slice(1, valueCount + 1); - } - } else { // [x, y] - for (i = 0; i < dataLength; i++) { - pt = data[i]; - xData[i] = pt[0]; - yData[i] = pt[1]; - } - } - } /* else { - error(12); // Highcharts expects configs to be numbers or arrays in turbo mode - }*/ - } else { - for (i = 0; i < dataLength; i++) { - pt = { series: series }; - pointProto.applyOptions.apply(pt, [data[i]]); - xData[i] = pt.x; - yData[i] = pointProto.toYData ? pointProto.toYData.apply(pt) : pt.y; - } - } - - series.data = []; - series.options.data = data; - series.xData = xData; - series.yData = yData; - - // destroy old points - i = (oldData && oldData.length) || 0; - while (i--) { - if (oldData[i] && oldData[i].destroy) { - oldData[i].destroy(); - } - } - - // reset minRange (#878) - if (xAxis) { - xAxis.minRange = xAxis.userMinRange; - } - - // redraw - series.isDirty = series.isDirtyData = 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; - }, - - /** - * Process the data by cropping away unused data points if the series is longer - * than the crop threshold. This saves computing time for lage series. - */ - processData: function (force) { - var series = this, - processedXData = series.xData, // copied during slice operation below - processedYData = series.yData, - dataLength = processedXData.length, - cropStart = 0, - cropEnd = dataLength, - cropped, - distance, - closestPointRange, - xAxis = series.xAxis, - i, // loop variable - options = series.options, - cropThreshold = options.cropThreshold, - isCartesian = series.isCartesian; - - // If the series data or axes haven't changed, don't go through this. Return false to pass - // the message on to override methods like in data grouping. - if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) { - return false; - } - - // optionally filter out points outside the plot area - if (isCartesian && series.sorted && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) { - var extremes = xAxis.getExtremes(), - min = extremes.min, - max = extremes.max; - - // it's outside current extremes - if (processedXData[dataLength - 1] < min || processedXData[0] > max) { - processedXData = []; - processedYData = []; - - // only crop if it's actually spilling out - } else if (processedXData[0] < min || processedXData[dataLength - 1] > max) { - - // iterate up to find slice start - for (i = 0; i < dataLength; i++) { - if (processedXData[i] >= min) { - cropStart = mathMax(0, i - 1); - break; - } - } - // proceed to find slice end - for (; i < dataLength; i++) { - if (processedXData[i] > max) { - cropEnd = i + 1; - break; - } - - } - processedXData = processedXData.slice(cropStart, cropEnd); - processedYData = processedYData.slice(cropStart, cropEnd); - cropped = true; - } - } - - - // Find the closest distance between processed points - for (i = processedXData.length - 1; i > 0; i--) { - distance = processedXData[i] - processedXData[i - 1]; - if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) { - closestPointRange = distance; - } - } - - // Record the properties - series.cropped = cropped; // undefined or true - series.cropStart = cropStart; - series.processedXData = processedXData; - series.processedYData = processedYData; - - if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC - series.pointRange = closestPointRange || 1; - } - series.closestPointRange = closestPointRange; - - }, - - /** - * Generate the data point after the data has been processed by cropping away - * unused points and optionally grouped in Highcharts Stock. - */ - generatePoints: function () { - var series = this, - options = series.options, - dataOptions = options.data, - data = series.data, - dataLength, - processedXData = series.processedXData, - processedYData = series.processedYData, - pointClass = series.pointClass, - processedDataLength = processedXData.length, - cropStart = series.cropStart || 0, - cursor, - hasGroupedData = series.hasGroupedData, - point, - points = [], - i; - - if (!data && !hasGroupedData) { - var arr = []; - arr.length = dataOptions.length; - data = series.data = arr; - } - - for (i = 0; i < processedDataLength; i++) { - cursor = cropStart + i; - if (!hasGroupedData) { - if (data[cursor]) { - point = data[cursor]; - } else if (dataOptions[cursor] !== UNDEFINED) { // #970 - data[cursor] = point = (new pointClass()).init(series, dataOptions[cursor], processedXData[i]); - } - points[i] = point; - } else { - // splat the y data in case of ohlc data array - points[i] = (new pointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i]))); - } - } - - // Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when - // swithching view from non-grouped data to grouped data (#637) - if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) { - for (i = 0; i < dataLength; i++) { - if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points - i += processedDataLength; - } - if (data[i]) { - data[i].destroyElements(); - data[i].plotX = UNDEFINED; // #1003 - } - } - } - - series.data = data; - series.points = points; - }, - - /** - * Translate data points from raw data values to chart specific positioning data - * needed later in drawPoints, drawGraph and drawTracker. - */ - translate: function () { - if (!this.processedXData) { // hidden series - this.processData(); - } - this.generatePoints(); - var series = this, - chart = series.chart, - options = series.options, - stacking = options.stacking, - xAxis = series.xAxis, - categories = xAxis.categories, - yAxis = series.yAxis, - points = series.points, - dataLength = points.length, - hasModifyValue = !!series.modifyValue, - isLastSeries, - allStackSeries = yAxis.series, - i = allStackSeries.length; - - // Is it the last visible series? - while (i--) { - if (allStackSeries[i].visible) { - if (i === series.index) { - isLastSeries = true; - } - break; - } - } - - // Translate each point - for (i = 0; i < dataLength; i++) { - var point = points[i], - xValue = point.x, - yValue = point.y, - yBottom = point.low, - stack = yAxis.stacks[(yValue < options.threshold ? '-' : '') + series.stackKey], - pointStack, - pointStackTotal; - - // get the plotX translation - //point.plotX = mathRound(xAxis.translate(xValue, 0, 0, 0, 1) * 10) / 10; // Math.round fixes #591 - point.plotX = xAxis.translate(xValue, 0, 0, 0, 1); // Math.round fixes #591 - - // 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 (isLastSeries) { - yBottom = options.threshold; - } - - 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; - point.stackY = yValue; - } - - // Set translated yBottom or remove it - point.yBottom = defined(yBottom) ? - yAxis.translate(yBottom, 0, 1, 0, 1) : - null; - - // general hook, used for Highstock compare mode - if (hasModifyValue) { - yValue = series.modifyValue(yValue, point); - } - - // Set the the plotY value, reset it for redraws - point.plotY = (typeof yValue === 'number') ? - mathRound(yAxis.translate(yValue, 0, 1, 0, 1) * 10) / 10 : // Math.round fixes #591 - UNDEFINED; - - // 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; - - - } - - // now that we have the cropped data, build the segments - series.getSegments(); - }, - /** - * Memoize tooltip texts and positions - */ - setTooltipPoints: function (renew) { - var series = this, - chart = series.chart, - points = [], - pointsLength, - plotSize = chart.plotSizeX, - low, - high, - xAxis = series.xAxis, - point, - i, - tooltipPoints = []; // a lookup array for each pixel in the x dimension - - // don't waste resources if tracker is disabled - if (series.options.enableMouseTracking === false) { - return; - } - - // renew - if (renew) { - series.tooltipPoints = null; - } - - // concat segments to overcome null values - each(series.segments || series.points, function (segment) { - points = points.concat(segment); - }); - - // loop the concatenated points and apply each point to all the closest - // pixel positions - if (xAxis && xAxis.reversed) { - points = points.reverse(); - } - - // Assign each pixel position to the nearest point - pointsLength = points.length; - for (i = 0; i < pointsLength; i++) { - point = points[i]; - low = points[i - 1] ? points[i - 1]._high + 1 : 0; - point._high = high = points[i + 1] ? - mathMax(0, mathFloor((point.plotX + (points[i + 1] ? points[i + 1].plotX : plotSize)) / 2)) : - plotSize; - - while (low >= 0 && low <= high) { - tooltipPoints[low++] = point; - } - } - series.tooltipPoints = tooltipPoints; - }, - - /** - * Format the header of the tooltip - */ - tooltipHeaderFormatter: function (key) { - var series = this, - tooltipOptions = series.tooltipOptions, - xDateFormat = tooltipOptions.xDateFormat, - xAxis = series.xAxis, - isDateTime = xAxis && xAxis.options.type === 'datetime', - n; - - // Guess the best date format based on the closest point distance (#568) - if (isDateTime && !xDateFormat) { - for (n in timeUnits) { - if (timeUnits[n] >= xAxis.closestPointRange) { - xDateFormat = tooltipOptions.dateTimeLabelFormats[n]; - break; - } - } - } - - return tooltipOptions.headerFormat - .replace('{point.key}', isDateTime ? dateFormat(xDateFormat, key) : key) - .replace('{series.name}', series.name) - .replace('{series.color}', series.color); - }, - - /** - * 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'); - } - - // 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.shared) { - 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, - points = series.points, - chart = series.chart, - plotX, - plotY, - i, - point, - radius, - symbol, - isImage, - graphic; - - if (series.options.marker.enabled) { - i = points.length; - while (i--) { - point = points[i]; - plotX = point.plotX; - plotY = point.plotY; - graphic = point.graphic; - - // only draw the point if y is defined - if (plotY !== UNDEFINED && !isNaN(plotY)) { - - // shortcuts - pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]; - radius = pointAttr.r; - symbol = pick(point.marker && point.marker.symbol, series.symbol); - isImage = symbol.indexOf('url') === 0; - - if (graphic) { // update - graphic.animate(extend({ - x: plotX - radius, - y: plotY - radius - }, graphic.symbolName ? { // don't apply to image symbols #507 - width: 2 * radius, - height: 2 * radius - } : {})); - } else if (radius > 0 || isImage) { - point.graphic = chart.renderer.symbol( - symbol, - plotX - radius, - plotY - radius, - 2 * radius, - 2 * 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 - }, - points = series.points || [], // #927 - 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 = points.length; - while (i--) { - point = points[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, - issue134 = /AppleWebKit\/533/.test(userAgent), - destroy, - i, - data = series.data || [], - point, - prop, - axis; - - // add event hook - fireEvent(series, 'destroy'); - - // remove all events - removeEvent(series); - - // erase from axes - each(['xAxis', 'yAxis'], function (AXIS) { - axis = series[AXIS]; - if (axis) { - erase(axis.series, series); - axis.isDirty = true; - } - }); - - // remove legend items - if (series.legendItem) { - series.chart.legend.destroyItem(series); - } - - // destroy all points with their elements - i = data.length; - while (i--) { - point = data[i]; - if (point && point.destroy) { - point.destroy(); - } - } - series.points = null; - - // 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', 'trackerGroup'], 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 () { - - var series = this, - seriesOptions = series.options, - options = seriesOptions.dataLabels; - - if (options.enabled || series._hasPointLabels) { - var x, - y, - points = series.points, - pointOptions, - generalOptions, - str, - dataLabelsGroup = series.dataLabelsGroup, - chart = series.chart, - xAxis = series.xAxis, - groupLeft = xAxis ? xAxis.left : chart.plotLeft, - yAxis = series.yAxis, - groupTop = yAxis ? yAxis.top : chart.plotTop, - renderer = chart.renderer, - inverted = chart.inverted, - seriesType = series.type, - stacking = seriesOptions.stacking, - isBarLike = seriesType === 'column' || seriesType === 'bar', - vAlignIsNull = options.verticalAlign === null, - yIsNull = options.y === null, - fontMetrics = renderer.fontMetrics(options.style.fontSize), // height and baseline - fontLineHeight = fontMetrics.h, - fontBaseline = fontMetrics.b, - dataLabel, - enabled; - - if (isBarLike) { - var defaultYs = { - top: fontBaseline, - middle: fontBaseline - fontLineHeight / 2, - bottom: -fontLineHeight + fontBaseline - }; - 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: defaultYs[options.verticalAlign]}); - } - } else { - // In non stacked series the default label placement is on top of the bars - if (vAlignIsNull) { - options = merge(options, {verticalAlign: 'top'}); - - // If no y delta is specified, try to create a good default (like default bar) - } else if (yIsNull) { - options = merge(options, { y: defaultYs[options.verticalAlign]}); - } - - } - } - - - // 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(groupLeft, groupTop) - .add(); - } else { - dataLabelsGroup.translate(groupLeft, groupTop); - } - - // make the labels for each point - generalOptions = options; - each(points, function (point) { - - dataLabel = point.dataLabel; - - // Merge in individual options from point - options = generalOptions; // reset changes from previous points - pointOptions = point.options; - if (pointOptions && pointOptions.dataLabels) { - options = merge(options, pointOptions.dataLabels); - } - enabled = options.enabled; - - // Get the positions - if (enabled) { - var plotX = (point.barX && point.barX + point.barW / 2) || pick(point.plotX, -999), - plotY = pick(point.plotY, -999), - - // if options.y is null, which happens by default on column charts, set the position - // above or below the column depending on the threshold - individualYDelta = options.y === null ? - (point.y >= seriesOptions.threshold ? - -fontLineHeight + fontBaseline : // below the threshold - fontBaseline) : // above the threshold - options.y; - - x = (inverted ? chart.plotWidth - plotY : plotX) + options.x; - y = mathRound((inverted ? chart.plotHeight - plotX : plotY) + individualYDelta); - - } - - // If the point is outside the plot area, destroy it. #678, #820 - if (dataLabel && series.isCartesian && (!chart.isInsidePlot(x, y) || !enabled)) { - point.dataLabel = dataLabel.destroy(); - - // Individual labels are disabled if the are explicitly disabled - // in the point options, or if they fall outside the plot area. - } else if (enabled) { - - var align = options.align, - attr, - name; - - // Get the string - str = options.formatter.call(point.getLabelConfig(), options); - - // in columns, align the string to the column - if (seriesType === 'column') { - x += { left: -1, right: 1 }[align] * point.barW / 2 || 0; - } - - if (!stacking && inverted && point.y < 0) { - align = 'right'; - x -= 10; - } - - // Determine the color - options.style.color = pick(options.color, options.style.color, series.color, 'black'); - - - // update existing label - if (dataLabel) { - // vertically centered - dataLabel - .attr({ - text: str - }).animate({ - x: x, - y: y - }); - // create new label - } else if (defined(str)) { - attr = { - align: align, - fill: options.backgroundColor, - stroke: options.borderColor, - 'stroke-width': options.borderWidth, - r: options.borderRadius || 0, - rotation: options.rotation, - padding: options.padding, - zIndex: 1 - }; - // Remove unused attributes (#947) - for (name in attr) { - if (attr[name] === UNDEFINED) { - delete attr[name]; - } - } - - dataLabel = point.dataLabel = renderer[options.rotation ? 'text' : 'label']( // labels don't support rotation - str, - x, - y, - null, - null, - null, - options.useHTML, - true // baseline for backwards compat - ) - .attr(attr) - .css(options.style) - .add(dataLabelsGroup) - .shadow(options.shadow); - } - - if (isBarLike && seriesOptions.stacking && dataLabel) { - var barX = point.barX, - 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 - }); - } - - - } - }); - } - }, - - /** - * Return the graph path of a segment - */ - getSegmentPath: function (segment) { - var series = this, - 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 && series.options.step) { - var lastPoint = segment[i - 1]; - segmentPath.push( - point.plotX, - lastPoint.plotY - ); - } - - // normal line to next point - segmentPath.push( - point.plotX, - point.plotY - ); - } - }); - - return segmentPath; - }, - - /** - * Draw the actual graph - */ - drawGraph: function () { - var series = this, - options = series.options, - chart = series.chart, - graph = series.graph, - graphPath = [], - group = series.group, - color = options.lineColor || series.color, - lineWidth = options.lineWidth, - dashStyle = options.dashStyle, - segmentPath, - renderer = chart.renderer, - singlePoints = [], // used in drawTracker - attribs; - - // divide into segments and build graph and area paths - each(series.segments, function (segment) { - - segmentPath = series.getSegmentPath(segment); - - // 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]); - } - }); - - // used in drawTracker: - series.graphPath = graphPath; - series.singlePoints = singlePoints; - - // 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); - } - } - }, - - /** - * Initialize and perform group inversion on series.group and series.trackerGroup - */ - invertGroups: function () { - var series = this, - group = series.group, - trackerGroup = series.trackerGroup, - chart = series.chart; - - // A fixed size is needed for inversion to work - function setInvert() { - var size = { - width: series.yAxis.len, - height: series.xAxis.len - }; - - // Set the series.group size - group.attr(size).invert(); - - // Set the tracker group size - if (trackerGroup) { - trackerGroup.attr(size).invert(); - } - } - - addEvent(chart, 'resize', setInvert); // do it on resize - addEvent(series, 'destroy', function () { - removeEvent(chart, 'resize', setInvert); - }); - - // Do it now - setInvert(); // do it now - - // On subsequent render and redraw, just do setInvert without setting up events again - series.invertGroups = setInvert; - }, - - /** - * Create the series group - */ - createGroup: function () { - - var chart = this.chart, - group = this.group = chart.renderer.g('series'); - - group.attr({ - visibility: this.visible ? VISIBLE : HIDDEN, - zIndex: this.options.zIndex - }) - .translate(this.xAxis.left, this.yAxis.top) - .add(chart.seriesGroup); - - // Only run this once - this.createGroup = noop; - }, - - /** - * Render the graph and markers - */ - render: function () { - var series = this, - chart = series.chart, - group, - options = series.options, - doClip = options.clip !== false, - 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. - - // Todo: since creating the clip property, the clipRect is created but - // never used when clip is false. A better way would be that the animation - // would run, then the clipRect destroyed. - if (!clipRect) { - clipRect = series.clipRect = !chart.hasRendered && chart.clipRect ? - chart.clipRect : - renderer.clipRect(0, 0, chart.plotSizeX, chart.plotSizeY + 1); - if (!chart.clipRect) { - chart.clipRect = clipRect; - } - } - - - // the group - series.createGroup(); - group = series.group; - - - 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(); - } - - // Handle inverted series and tracker groups - if (chart.inverted) { - series.invertGroups(); - } - - // Do the initial clipping. This must be done after inverting for VML. - if (doClip && !series.hasRendered) { - group.clip(clipRect); - if (series.trackerGroup) { - series.trackerGroup.clip(chart.clipRect); - } - } - - - // 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) { - if (doClip) { - group.clip((series.clipRect = chart.clipRect)); - } - clipRect.destroy(); - } - }, duration); - - series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see - // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see - series.hasRendered = true; - }, - - /** - * Redraw the series after an update in the axes. - */ - redraw: function () { - var series = this, - chart = series.chart, - wasDirtyData = series.isDirtyData, // cache it here as it is set to false in render, but used after - group = series.group; - - // reposition on resize - if (group) { - if (chart.inverted) { - group.attr({ - width: chart.plotWidth, - height: chart.plotHeight - }); - } - - group.animate({ - translateX: series.xAxis.left, - translateY: series.yAxis.top - }); - } - - series.translate(); - series.setTooltipPoints(true); - - series.render(); - if (wasDirtyData) { - fireEvent(series, 'updatedData'); - } - }, - - /** - * 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, - points = series.points, - 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 if (points) { - i = points.length; - while (i--) { - point = points[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'); - }, - - /** - * Create a group that holds the tracking object or objects. This allows for - * individual clipping and placement of each series tracker. - */ - drawTrackerGroup: function () { - var trackerGroup = this.trackerGroup, - chart = this.chart; - - if (this.isCartesian) { - - // Generate it on first call - if (!trackerGroup) { - this.trackerGroup = trackerGroup = chart.renderer.g() - .attr({ - zIndex: this.options.zIndex || 1 - }) - .add(chart.trackerGroup); - - } - // Place it on first and subsequent (redraw) calls - trackerGroup.translate(this.xAxis.left, this.yAxis.top); - - } - - return trackerGroup; - }, - - /** - * 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, - trackByArea = options.trackByArea, - trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath), - trackerPathLength = trackerPath.length, - chart = series.chart, - renderer = chart.renderer, - snap = chart.options.tooltip.snap, - tracker = series.tracker, - cursor = options.cursor, - css = cursor && { cursor: cursor }, - singlePoints = series.singlePoints, - trackerGroup = series.drawTrackerGroup(), - singlePoint, - i; - - // Extend end points. A better way would be to use round linecaps, - // but those are not clickable in VML. - if (trackerPathLength && !trackByArea) { - 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 = renderer.path(trackerPath) - .attr({ - isTracker: true, - 'stroke-linejoin': 'bevel', - visibility: series.visible ? VISIBLE : HIDDEN, - stroke: TRACKER_FILL, - fill: trackByArea ? TRACKER_FILL : NONE, - 'stroke-width' : options.lineWidth + (trackByArea ? 0 : 2 * snap) - }) - .on(hasTouch ? 'touchstart' : 'mouseover', function () { - if (chart.hoverSeries !== series) { - series.onMouseOver(); - } - }) - .on('mouseout', function () { - if (!options.stickyTracking) { - series.onMouseOut(); - } - }) - .css(css) - .add(trackerGroup); - } - - } - -}; // end Series prototype - - -/** - * LineSeries object - */ -var LineSeries = extendClass(Series); -seriesTypes.line = LineSeries; - -/** - * Set the default options for area - */ -defaultPlotOptions.area = merge(defaultSeriesOptions, { - threshold: 0 - // trackByArea: false, - // lineColor: null, // overrides color, but lets fillColor be unaltered - // fillOpacity: 0.75, - // fillColor: null -}); - -/** - * AreaSeries object - */ -var AreaSeries = extendClass(Series, { - type: 'area', - - /** - * Extend the base Series getSegmentPath method by adding the path for the area. - * This path is pushed to the series.areaPath property. - */ - getSegmentPath: function (segment) { - - var segmentPath = Series.prototype.getSegmentPath.call(this, segment), // call base method - areaSegmentPath = [].concat(segmentPath), // work on a copy for the area path - i, - options = this.options, - segLength = segmentPath.length, - translatedThreshold = this.yAxis.getThreshold(options.threshold); - - if (segLength === 3) { // for animation from 1 to two points - areaSegmentPath.push(L, segmentPath[1], segmentPath[2]); - } - if (options.stacking && this.type !== 'areaspline') { - - // Follow stack back. Todo: implement areaspline. A general solution could be to - // reverse the entire graphPath of the previous series, though may be hard with - // splines and with series with different extremes - for (i = segment.length - 1; i >= 0; i--) { - - // step line? - if (i < segment.length - 1 && options.step) { - areaSegmentPath.push(segment[i + 1].plotX, segment[i].yBottom); - } - - 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 - ); - } - this.areaPath = this.areaPath.concat(areaSegmentPath); - - return segmentPath; - }, - - /** - * Draw the graph and the underlying area. This method calls the Series base - * function and adds the area. The areaPath is calculated in the getSegmentPath - * method called from Series.prototype.drawGraph. - */ - drawGraph: function () { - - // Define or reset areaPath - this.areaPath = []; - - // Call the base method - Series.prototype.drawGraph.apply(this); - - // Define local variables - var areaPath = this.areaPath, - options = this.options, - area = this.area; - - // Create or update the area - if (area) { // update - area.animate({ d: areaPath }); - - } else { // create - this.area = this.chart.renderer.path(areaPath) - .attr({ - fill: pick( - options.fillColor, - Color(this.color).setOpacity(options.fillOpacity || 0.75).get() - ) - }).add(this.group); - } - }, - - /** - * Get the series' symbol in the legend - * - * @param {Object} legend The legend object - * @param {Object} item The series (this) or point - */ - drawLegendSymbol: function (legend, item) { - - item.legendSymbol = this.chart.renderer.rect( - 0, - legend.baseline - 11, - legend.options.symbolWidth, - 12, - 2 - ).attr({ - zIndex: 3 - }).add(item.legendGroup); - - } -}); - -seriesTypes.area = AreaSeries;/** - * Set the default options for spline - */ -defaultPlotOptions.spline = merge(defaultSeriesOptions); - -/** - * 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; - -/** - * Set the default options for areaspline - */ -defaultPlotOptions.areaspline = merge(defaultPlotOptions.area); - -/** - * AreaSplineSeries object - */ -var areaProto = AreaSeries.prototype, - AreaSplineSeries = extendClass(SplineSeries, { - type: 'areaspline', - - // Mix in methods from the area series - getSegmentPath: areaProto.getSegmentPath, - drawGraph: areaProto.drawGraph - }); -seriesTypes.areaspline = AreaSplineSeries; - -/** - * Set the default options for column - */ -defaultPlotOptions.column = merge(defaultSeriesOptions, { - borderColor: '#FFFFFF', - borderWidth: 1, - borderRadius: 0, - //colorByPoint: undefined, - groupPadding: 0.2, - marker: null, // point options are specified in the base options - pointPadding: 0.1, - //pointWidth: null, - minPointLength: 0, - cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes - pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories - states: { - hover: { - brightness: 0.1, - shadow: false - }, - select: { - color: '#C0C0C0', - borderColor: '#000000', - shadow: false - } - }, - dataLabels: { - y: null, - verticalAlign: null - }, - threshold: 0 -}); - -/** - * ColumnSeries object - */ -var ColumnSeries = extendClass(Series, { - type: 'column', - tooltipOutsidePlot: true, - 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; - - // 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, - xAxis = series.xAxis, - reversedXAxis = xAxis.reversed, - 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 && - series.options.group === otherSeries.options.group) { // used in Stock charts navigator series - 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 points = series.points, - categoryWidth = mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || 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 = pick(optionPointWidth, pointOffsetWidth - 2 * pointPadding), // exact point width, used in polar charts - barW = mathCeil(mathMax(pointWidth, 1 + 2 * borderWidth)), // rounded and postprocessed for border width - colIndex = (reversedXAxis ? columnCount - - series.columnIndex : series.columnIndex) || 0, - pointXOffset = pointPadding + (groupPadding + colIndex * - pointOffsetWidth - (categoryWidth / 2)) * - (reversedXAxis ? -1 : 1), - threshold = options.threshold, - translatedThreshold = series.yAxis.getThreshold(threshold), - minPointLength = pick(options.minPointLength, 5); - - // record the new values - each(points, function (point) { - var plotY = point.plotY, - yBottom = pick(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], - 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, barW); - } - - // handle options.minPointLength - if (mathAbs(barH) < minPointLength) { - if (minPointLength) { - barH = minPointLength; - barY = - mathAbs(barY - translatedThreshold) > minPointLength ? // stacked - yBottom - minPointLength : // keep position - translatedThreshold - (plotY <= translatedThreshold ? minPointLength : 0); - } - } - - extend(point, { - barX: barX, - barY: barY, - barW: barW, - barH: barH, - pointWidth: pointWidth - }); - - // create shape type and shape args that are reused in drawPoints and drawTracker - point.shapeType = 'rect'; - point.shapeArgs = shapeArgs = chart.renderer.Element.prototype.crisp.call(0, borderWidth, barX, barY, barW, barH); - - if (borderWidth % 2) { // correct for shorting in crisp method, visible in stacked columns with 1px border - shapeArgs.y -= 1; - shapeArgs.height += 1; - } - - // make small columns responsive to mouse - point.trackerArgs = mathAbs(barH) < 3 && merge(point.shapeArgs, { - height: 6, - y: barY - 3 - }); - }); - - }, - - getSymbol: function () { - }, - - /** - * Use a solid rectangle like the area series types - */ - drawLegendSymbol: AreaSeries.prototype.drawLegendSymbol, - - - /** - * 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.points, 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(merge(shapeArgs)); - - } else { - point.graphic = graphic = renderer[point.shapeType](shapeArgs) - .attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]) - .add(series.group) - .shadow(options.shadow, null, options.stacking && !options.borderRadius); - } - - } - }); - }, - /** - * 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 }, - trackerGroup = series.drawTrackerGroup(), - rel, - plotY, - validPlotY; - - each(series.points, function (point) { - tracker = point.tracker; - shapeArgs = point.trackerArgs || point.shapeArgs; - plotY = point.plotY; - validPlotY = !series.isCartesian || (plotY !== UNDEFINED && !isNaN(plotY)); - delete shapeArgs.strokeWidth; - if (point.y !== null && validPlotY) { - if (tracker) {// update - tracker.attr(shapeArgs); - - } else { - point.tracker = - renderer[point.shapeType](shapeArgs) - .attr({ - isTracker: trackerLabel, - fill: TRACKER_FILL, - visibility: series.visible ? VISIBLE : HIDDEN - }) - .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 || 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, - points = series.points, - options = series.options; - - 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(points, function (point) { - var graphic = point.graphic, - shapeArgs = point.shapeArgs, - yAxis = series.yAxis, - threshold = options.threshold; - - if (graphic) { - // start values - graphic.attr({ - height: 0, - y: defined(threshold) ? - yAxis.getThreshold(threshold) : - yAxis.translate(yAxis.getExtremes().min, 0, 1, 0, 1) - }); - - // animate - graphic.animate({ - height: shapeArgs.height, - y: shapeArgs.y - }, 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; -/** - * Set the default options for bar - */ -defaultPlotOptions.bar = merge(defaultPlotOptions.column, { - dataLabels: { - align: 'left', - x: 5, - y: null, - verticalAlign: 'middle' - } -}); -/** - * The Bar series class - */ -var BarSeries = extendClass(ColumnSeries, { - type: 'bar', - inverted: true -}); -seriesTypes.bar = BarSeries; - -/** - * Set the default options for scatter - */ -defaultPlotOptions.scatter = merge(defaultSeriesOptions, { - lineWidth: 0, - states: { - hover: { - lineWidth: 0 - } - }, - tooltip: { - headerFormat: '{series.name}
', - pointFormat: 'x: {point.x}
y: {point.y}
' - } -}); - -/** - * The scatter series class - */ -var ScatterSeries = extendClass(Series, { - type: 'scatter', - sorted: false, - /** - * 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.points, function (point) { - point.shapeType = 'circle'; - point.shapeArgs = { - x: point.plotX, - y: point.plotY, - r: series.chart.options.tooltip.snap - }; - }); - }, - - /** - * Add tracking event listener to the series group, so the point graphics - * themselves act as trackers - */ - drawTracker: function () { - var series = this, - cursor = series.options.cursor, - css = cursor && { cursor: cursor }, - points = series.points, - i = points.length, - graphic; - - // Set an expando property for the point index, used below - while (i--) { - graphic = points[i].graphic; - if (graphic) { // doesn't exist for null points - graphic.element._i = i; - } - } - - // Add the event listeners, we need to do this only once - if (!series._hasTracking) { - series.group - .attr({ - isTracker: true - }) - .on(hasTouch ? 'touchstart' : 'mouseover', function (e) { - series.onMouseOver(); - if (e.target._i !== UNDEFINED) { // undefined on graph in scatterchart - points[e.target._i].onMouseOver(); - } - }) - .on('mouseout', function () { - if (!series.options.stickyTracking) { - series.onMouseOut(); - } - }) - .css(css); - } else { - series._hasTracking = true; - } - - } -}); -seriesTypes.scatter = ScatterSeries; - -/** - * Set the default options for pie - */ -defaultPlotOptions.pie = merge(defaultSeriesOptions, { - borderColor: '#FFFFFF', - borderWidth: 1, - center: ['50%', '50%'], - colorByPoint: true, // always true for pies - dataLabels: { - // align: null, - // connectorWidth: 1, - // connectorColor: point.color, - // connectorPadding: 5, - distance: 30, - enabled: true, - formatter: function () { - return this.point.name; - }, - // softConnector: true, - y: 5 - }, - //innerSize: 0, - legendType: 'point', - marker: null, // point options are specified in the base options - size: '75%', - showInLegend: false, - slicedOffset: 10, - states: { - hover: { - brightness: 0.1, - shadow: false - } - } -}); - -/** - * 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 = { - 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 pies in - */ - animate: function () { - var series = this, - points = series.points; - - each(points, 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; - - }, - - /** - * Extend the basic setData method by running processData and generatePoints immediately, - * in order to access the points from the legend. - */ - setData: function (data, redraw) { - Series.prototype.setData.call(this, data, false); - this.processData(); - this.generatePoints(); - if (pick(redraw, true)) { - this.chart.redraw(); - } - }, - - /** - * Get the center of the pie based on the size and center options relative to the - * plot area. Borrowed by the polar and gauge series types. - */ - getCenter: function () { - - var options = this.options, - chart = this.chart, - plotWidth = chart.plotWidth, - plotHeight = chart.plotHeight, - positions = options.center.concat([options.size, options.innerSize || 0]), - smallestSize = mathMin(plotWidth, plotHeight), - isPercent; - - return 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; - }); - }, - - /** - * Do translation for pie slices - */ - translate: function () { - this.generatePoints(); - - 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, - chart = series.chart, - start, - end, - angle, - points = series.points, - circ = 2 * mathPI, - fraction, - 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 - series.center = positions = series.getCenter(); - - // 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)); - }; - - // get the total sum - each(points, function (point) { - total += point.y; - }); - - each(points, 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(); - } - - // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see - 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.points, 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 = graphic = renderer.arc(shapeArgs) - .setRadialReference(series.center) - .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 && (halves[0][0].dataLabel.getBBox().height || 21); // 21 is for #968 - - /* Loop over the points in each half, 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; - - // Only do anti-collision when we are outside the pie and have connectors (#856) - if (distanceOption > 0) { - - // 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++) { - - var slot, naturalY; - - point = points[j]; - labelPos = point.labelPos; - dataLabel = point.dataLabel; - visibility = point.visible === false ? HIDDEN : VISIBLE; - naturalY = labelPos[1]; - - if (distanceOption > 0) { - slot = usedSlots.pop(); - 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; - } - - } else { - 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 = options.justify ? - seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption) : - 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, - - /** - * Use a simple symbol from column prototype - */ - drawLegendSymbol: AreaSeries.prototype.drawLegendSymbol, - - /** - * Pies don't have point marker symbols - */ - getSymbol: function () {} - -}; -PieSeries = extendClass(Series, PieSeries); -seriesTypes.pie = PieSeries; - - -// global variables -extend(Highcharts, { - - // Constructors - Axis: Axis, - CanVGRenderer: CanVGRenderer, - Chart: Chart, - Color: Color, - Legend: Legend, - Point: Point, - Tick: Tick, - Tooltip: Tooltip, - Renderer: Renderer, - Series: Series, - SVGRenderer: SVGRenderer, - VMLRenderer: VMLRenderer, - - // Various - dateFormat: dateFormat, - pathAnim: pathAnim, - getOptions: getOptions, - hasBidiBug: hasBidiBug, - numberFormat: numberFormat, - seriesTypes: seriesTypes, - setOptions: setOptions, - addEvent: addEvent, - removeEvent: removeEvent, - createElement: createElement, - discardElement: discardElement, - css: css, - each: each, - extend: extend, - map: map, - merge: merge, - pick: pick, - splat: splat, - extendClass: extendClass, - pInt: pInt, - product: 'Highcharts', - version: '2.2.5' -}); -}()); + var UNDEFINED, + doc = win.document, + math = Math, + mathRound = math.round, + mathFloor = math.floor, + mathCeil = math.ceil, + mathMax = math.max, + mathMin = math.min, + mathAbs = math.abs, + mathCos = math.cos, + mathSin = math.sin, + mathPI = math.PI, + deg2rad = mathPI * 2 / 360, + + + // some variables + userAgent = (win.navigator && win.navigator.userAgent) || '', + isOpera = win.opera, + isMS = /(msie|trident|edge)/i.test(userAgent) && !isOpera, + docMode8 = doc && doc.documentMode === 8, + isWebKit = !isMS && /AppleWebKit/.test(userAgent), + isFirefox = /Firefox/.test(userAgent), + isTouchDevice = /(Mobile|Android|Windows Phone)/.test(userAgent), + SVG_NS = 'http://www.w3.org/2000/svg', + hasSVG = doc && doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect, + hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4, // issue #38 + useCanVG = doc && !hasSVG && !isMS && !!doc.createElement('canvas').getContext, + Renderer, + hasTouch, + symbolSizes = {}, + idCounter = 0, + garbageBin, + defaultOptions, + dateFormat, // function + pathAnim, + timeUnits, + noop = function () {}, + charts = [], + chartCount = 0, + PRODUCT = 'Highcharts', + VERSION = '4.2.5-modified', + + // some constants for frequently used strings + DIV = 'div', + ABSOLUTE = 'absolute', + RELATIVE = 'relative', + HIDDEN = 'hidden', + PREFIX = 'highcharts-', + VISIBLE = 'visible', + PX = 'px', + NONE = 'none', + M = 'M', + L = 'L', + numRegex = /^[0-9]+$/, + NORMAL_STATE = '', + HOVER_STATE = 'hover', + SELECT_STATE = 'select', + marginNames = ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'], + + // Object for extending Axis + AxisPlotLineOrBandExtension, + + // constants for attributes + STROKE_WIDTH = 'stroke-width', + + // time methods, changed based on whether or not UTC is used + Date, // Allow using a different Date class + makeTime, + timezoneOffset, + getTimezoneOffset, + getMinutes, + getHours, + getDay, + getDate, + getMonth, + getFullYear, + setMilliseconds, + setSeconds, + setMinutes, + setHours, + setDate, + setMonth, + setFullYear, + + + // lookup over the types and the associated classes + seriesTypes = {}, + Highcharts; + + /** + * Provide error messages for debugging, with links to online explanation + */ + function error(code, stop) { + var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code; + if (stop) { + throw new Error(msg); + } + // else ... + if (win.console) { + console.log(msg); // eslint-disable-line no-console + } + } + + // The Highcharts namespace + Highcharts = win.Highcharts ? error(16, true) : { win: win }; + + Highcharts.seriesTypes = seriesTypes; + var timers = [], + getStyle, + + // Previous adapter functions + inArray, + each, + grep, + offset, + map, + addEvent, + removeEvent, + fireEvent, + animate, + stop; + + /** + * An animator object. One instance applies to one property (attribute or style prop) + * on one element. + * + * @param {object} elem The element to animate. May be a DOM element or a Highcharts SVGElement wrapper. + * @param {object} options Animation options, including duration, easing, step and complete. + * @param {object} prop The property to animate. + */ + function Fx(elem, options, prop) { + this.options = options; + this.elem = elem; + this.prop = prop; + } + Fx.prototype = { + + /** + * Animating a path definition on SVGElement + * @returns {undefined} + */ + dSetter: function () { + var start = this.paths[0], + end = this.paths[1], + ret = [], + now = this.now, + i = start.length, + startVal; + + if (now === 1) { // land on the final path without adjustment points appended in the ends + ret = this.toD; + + } else if (i === end.length && now < 1) { + while (i--) { + startVal = parseFloat(start[i]); + ret[i] = + isNaN(startVal) ? // a letter instruction like M or L + start[i] : + now * (parseFloat(end[i] - startVal)) + startVal; + + } + } else { // if animation is finished or length not matching, land on right value + ret = end; + } + this.elem.attr('d', ret); + }, + + /** + * Update the element with the current animation step + * @returns {undefined} + */ + update: function () { + var elem = this.elem, + prop = this.prop, // if destroyed, it is null + now = this.now, + step = this.options.step; + + // Animation setter defined from outside + if (this[prop + 'Setter']) { + this[prop + 'Setter'](); + + // Other animations on SVGElement + } else if (elem.attr) { + if (elem.element) { + elem.attr(prop, now); + } + + // HTML styles, raw HTML content like container size + } else { + elem.style[prop] = now + this.unit; + } + + if (step) { + step.call(elem, now, this); + } + + }, + + /** + * Run an animation + */ + run: function (from, to, unit) { + var self = this, + timer = function (gotoEnd) { + return timer.stopped ? false : self.step(gotoEnd); + }, + i; + + this.startTime = +new Date(); + this.start = from; + this.end = to; + this.unit = unit; + this.now = this.start; + this.pos = 0; + + timer.elem = this.elem; + + if (timer() && timers.push(timer) === 1) { + timer.timerId = setInterval(function () { + + for (i = 0; i < timers.length; i++) { + if (!timers[i]()) { + timers.splice(i--, 1); + } + } + + if (!timers.length) { + clearInterval(timer.timerId); + } + }, 13); + } + }, + + /** + * Run a single step in the animation + * @param {Boolean} gotoEnd Whether to go to then endpoint of the animation after abort + * @returns {Boolean} True if animation continues + */ + step: function (gotoEnd) { + var t = +new Date(), + ret, + done, + options = this.options, + elem = this.elem, + complete = options.complete, + duration = options.duration, + curAnim = options.curAnim, + i; + + if (elem.attr && !elem.element) { // #2616, element including flag is destroyed + ret = false; + + } else if (gotoEnd || t >= duration + this.startTime) { + this.now = this.end; + this.pos = 1; + this.update(); + + curAnim[this.prop] = true; + + done = true; + for (i in curAnim) { + if (curAnim[i] !== true) { + done = false; + } + } + + if (done && complete) { + complete.call(elem); + } + ret = false; + + } else { + this.pos = options.easing((t - this.startTime) / duration); + this.now = this.start + ((this.end - this.start) * this.pos); + this.update(); + ret = true; + } + return ret; + }, + + /** + * Prepare start and end values so that the path can be animated one to one + */ + initPath: function (elem, fromD, toD) { + fromD = fromD || ''; + var shift = elem.shift, + bezier = fromD.indexOf('C') > -1, + numParams = bezier ? 7 : 3, + endLength, + slice, + i, + start = fromD.split(' '), + end = [].concat(toD), // copy + isArea = elem.isArea, + positionFactor = isArea ? 2 : 1, + sixify = function (arr) { // in splines make move points have six parameters like bezier curves + i = arr.length; + while (i--) { + if (arr[i] === M || arr[i] === L) { + arr.splice(i + 1, 0, arr[i + 1], arr[i + 2], arr[i + 1], arr[i + 2]); + } + } + }; + + if (bezier) { + sixify(start); + sixify(end); + } + + // If shifting points, prepend a dummy point to the end path. For areas, + // prepend both at the beginning and end of the path. + if (shift <= end.length / numParams && start.length === end.length) { + while (shift--) { + end = end.slice(0, numParams).concat(end); + if (isArea) { + end = end.concat(end.slice(end.length - numParams)); + } + } + } + elem.shift = 0; // reset for following animations + + + // Copy and append last point until the length matches the end length + if (start.length) { + endLength = end.length; + while (start.length < endLength) { + + // Pull out the slice that is going to be appended or inserted. In a line graph, + // the positionFactor is 1, and the last point is sliced out. In an area graph, + // the positionFactor is 2, causing the middle two points to be sliced out, since + // an area path starts at left, follows the upper path then turns and follows the + // bottom back. + slice = start.slice().splice( + (start.length / positionFactor) - numParams, + numParams * positionFactor + ); + + // Disable first control point + if (bezier) { + slice[numParams - 6] = slice[numParams - 2]; + slice[numParams - 5] = slice[numParams - 1]; + } + + // Now insert the slice, either in the middle (for areas) or at the end (for lines) + [].splice.apply( + start, + [(start.length / positionFactor), 0].concat(slice) + ); + + } + } + + return [start, end]; + } + }; // End of Fx prototype + + + /** + * Extend an object with the members of another + * @param {Object} a The object to be extended + * @param {Object} b The object to add to the first one + */ + var extend = Highcharts.extend = function (a, b) { + var n; + if (!a) { + a = {}; + } + for (n in b) { + a[n] = b[n]; + } + return a; + }; + + /** + * Deep merge two or more objects and return a third object. If the first argument is + * true, the contents of the second object is copied into the first object. + * Previously this function redirected to jQuery.extend(true), but this had two limitations. + * First, it deep merged arrays, which lead to workarounds in Highcharts. Second, + * it copied properties from extended prototypes. + */ + function merge() { + var i, + args = arguments, + len, + ret = {}, + doCopy = function (copy, original) { + var value, key; + + // An object is replacing a primitive + if (typeof copy !== 'object') { + copy = {}; + } + + for (key in original) { + if (original.hasOwnProperty(key)) { + value = original[key]; + + // Copy the contents of objects, but not arrays or DOM nodes + if (value && typeof value === 'object' && Object.prototype.toString.call(value) !== '[object Array]' && + key !== 'renderTo' && typeof value.nodeType !== 'number') { + copy[key] = doCopy(copy[key] || {}, value); + + // Primitives and arrays are copied over directly + } else { + copy[key] = original[key]; + } + } + } + return copy; + }; + + // If first argument is true, copy into the existing object. Used in setOptions. + if (args[0] === true) { + ret = args[1]; + args = Array.prototype.slice.call(args, 2); + } + + // For each argument, extend the return + len = args.length; + for (i = 0; i < len; i++) { + ret = doCopy(ret, args[i]); + } + + return ret; + } + + /** + * Shortcut for parseInt + * @param {Object} s + * @param {Number} mag Magnitude + */ + function pInt(s, mag) { + return parseInt(s, mag || 10); + } + + /** + * Check for string + * @param {Object} s + */ + function isString(s) { + return typeof s === 'string'; + } + + /** + * Check for object + * @param {Object} obj + */ + function isObject(obj) { + return obj && typeof obj === 'object'; + } + + /** + * Check for array + * @param {Object} obj + */ + function isArray(obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + } + + /** + * Check for number + * @param {Object} n + */ + var isNumber = Highcharts.isNumber = function isNumber(n) { + return typeof n === 'number' && !isNaN(n); + }; + + /** + * Remove last occurence of an item from an array + * @param {Array} arr + * @param {Mixed} item + */ + function erase(arr, item) { + var i = arr.length; + while (i--) { + if (arr[i] === item) { + arr.splice(i, 1); + break; + } + } + //return arr; + } + + /** + * Returns true if the object is not null or undefined. + * @param {Object} obj + */ + function defined(obj) { + return obj !== UNDEFINED && obj !== null; + } + + /** + * Set or get an attribute or an object of attributes. Can't use jQuery attr because + * it attempts to set expando properties on the SVG element, which is not allowed. + * + * @param {Object} elem The DOM element to receive the attribute(s) + * @param {String|Object} prop The property or an abject of key-value pairs + * @param {String} value The value if a single property is set + */ + function attr(elem, prop, value) { + var key, + ret; + + // if the prop is a string + if (isString(prop)) { + // set the value + if (defined(value)) { + elem.setAttribute(prop, value); + + // get the value + } else if (elem && elem.getAttribute) { // elem not defined when printing pie demo... + ret = elem.getAttribute(prop); + } + + // else if prop is defined, it is a hash of key/value pairs + } else if (defined(prop) && isObject(prop)) { + for (key in prop) { + elem.setAttribute(key, prop[key]); + } + } + return ret; + } + /** + * Check if an element is an array, and if not, make it into an array. + */ + function splat(obj) { + return isArray(obj) ? obj : [obj]; + } + + /** + * Set a timeout if the delay is given, otherwise perform the function synchronously + * @param {Function} fn The function to perform + * @param {Number} delay Delay in milliseconds + * @param {Ojbect} context The context + * @returns {Nubmer} An identifier for the timeout + */ + function syncTimeout(fn, delay, context) { + if (delay) { + return setTimeout(fn, delay, context); + } + fn.call(0, context); + } + + + /** + * Return the first value that is defined. + */ + var pick = Highcharts.pick = function () { + var args = arguments, + i, + arg, + length = args.length; + for (i = 0; i < length; i++) { + arg = args[i]; + if (arg !== UNDEFINED && arg !== null) { + return arg; + } + } + }; + + /** + * Set CSS on a given element + * @param {Object} el + * @param {Object} styles Style object with camel case property names + */ + function css(el, styles) { + if (isMS && !hasSVG) { // #2686 + if (styles && styles.opacity !== UNDEFINED) { + styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')'; + } + } + extend(el.style, styles); + } + + /** + * Utility function to create element with attributes and styles + * @param {Object} tag + * @param {Object} attribs + * @param {Object} styles + * @param {Object} parent + * @param {Object} nopad + */ + function createElement(tag, attribs, styles, parent, nopad) { + var el = doc.createElement(tag); + if (attribs) { + extend(el, attribs); + } + if (nopad) { + css(el, { padding: 0, border: 'none', margin: 0 }); + } + if (styles) { + css(el, styles); + } + if (parent) { + parent.appendChild(el); + } + return el; + } + + /** + * Extend a prototyped class by new members + * @param {Object} parent + * @param {Object} members + */ + function extendClass(Parent, members) { + var object = function () { + }; + object.prototype = new Parent(); + extend(object.prototype, members); + return object; + } + + /** + * Pad a string to a given length by adding 0 to the beginning + * @param {Number} number + * @param {Number} length + */ + function pad(number, length, padder) { + return new Array((length || 2) + 1 - String(number).length).join(padder || 0) + number; + } + + /** + * Return a length based on either the integer value, or a percentage of a base. + */ + function relativeLength(value, base) { + return (/%$/).test(value) ? base * parseFloat(value) / 100 : parseFloat(value); + } + + /** + * Wrap a method with extended functionality, preserving the original function + * @param {Object} obj The context object that the method belongs to + * @param {String} method The name of the method to extend + * @param {Function} func A wrapper function callback. This function is called with the same arguments + * as the original function, except that the original function is unshifted and passed as the first + * argument. + */ + var wrap = Highcharts.wrap = function (obj, method, func) { + var proceed = obj[method]; + obj[method] = function () { + var args = Array.prototype.slice.call(arguments); + args.unshift(proceed); + return func.apply(this, args); + }; + }; + + + function getTZOffset(timestamp) { + return ((getTimezoneOffset && getTimezoneOffset(timestamp)) || timezoneOffset || 0) * 60000; + } + + /** + * Based on http://www.php.net/manual/en/function.strftime.php + * @param {String} format + * @param {Number} timestamp + * @param {Boolean} capitalize + */ + dateFormat = function (format, timestamp, capitalize) { + if (!defined(timestamp) || isNaN(timestamp)) { + return defaultOptions.lang.invalidDate || ''; + } + format = pick(format, '%Y-%m-%d %H:%M:%S'); + + var date = new Date(timestamp - getTZOffset(timestamp)), + key, // used in for constuct below + // get the basic time values + hours = date[getHours](), + day = date[getDay](), + dayOfMonth = date[getDate](), + month = date[getMonth](), + fullYear = date[getFullYear](), + lang = defaultOptions.lang, + langWeekdays = lang.weekdays, + shortWeekdays = lang.shortWeekdays, + + // List all format keys. Custom formats can be added from the outside. + replacements = extend({ + + // Day + 'a': shortWeekdays ? shortWeekdays[day] : langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon' + 'A': langWeekdays[day], // Long weekday, like 'Monday' + 'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31 + 'e': pad(dayOfMonth, 2, ' '), // Day of the month, 1 through 31 + 'w': day, + + // Week (none implemented) + //'W': weekNumber(), + + // Month + 'b': lang.shortMonths[month], // Short month, like 'Jan' + 'B': lang.months[month], // Long month, like 'January' + 'm': pad(month + 1), // Two digit month number, 01 through 12 + + // Year + 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009 + 'Y': fullYear, // Four digits year, like 2009 + + // Time + 'H': pad(hours), // Two digits hours in 24h format, 00 through 23 + 'k': hours, // Hours in 24h format, 0 through 23 + 'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11 + 'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12 + 'M': pad(date[getMinutes]()), // Two digits minutes, 00 through 59 + 'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM + 'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM + 'S': pad(date.getSeconds()), // Two digits seconds, 00 through 59 + 'L': pad(mathRound(timestamp % 1000), 3) // Milliseconds (naming from Ruby) + }, Highcharts.dateFormats); + + + // do the replaces + for (key in replacements) { + while (format.indexOf('%' + key) !== -1) { // regex would do it in one line, but this is faster + format = format.replace('%' + key, typeof replacements[key] === 'function' ? replacements[key](timestamp) : replacements[key]); + } + } + + // Optionally capitalize the string and return + return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format; + }; + + /** + * Format a single variable. Similar to sprintf, without the % prefix. + */ + function formatSingle(format, val) { + var floatRegex = /f$/, + decRegex = /\.([0-9])/, + lang = defaultOptions.lang, + decimals; + + if (floatRegex.test(format)) { // float + decimals = format.match(decRegex); + decimals = decimals ? decimals[1] : -1; + if (val !== null) { + val = Highcharts.numberFormat( + val, + decimals, + lang.decimalPoint, + format.indexOf(',') > -1 ? lang.thousandsSep : '' + ); + } + } else { + val = dateFormat(format, val); + } + return val; + } + + /** + * Format a string according to a subset of the rules of Python's String.format method. + */ + function format(str, ctx) { + var splitter = '{', + isInside = false, + segment, + valueAndFormat, + path, + i, + len, + ret = [], + val, + index; + + while ((index = str.indexOf(splitter)) !== -1) { + + segment = str.slice(0, index); + if (isInside) { // we're on the closing bracket looking back + + valueAndFormat = segment.split(':'); + path = valueAndFormat.shift().split('.'); // get first and leave format + len = path.length; + val = ctx; + + // Assign deeper paths + for (i = 0; i < len; i++) { + val = val[path[i]]; + } + + // Format the replacement + if (valueAndFormat.length) { + val = formatSingle(valueAndFormat.join(':'), val); + } + + // Push the result and advance the cursor + ret.push(val); + + } else { + ret.push(segment); + + } + str = str.slice(index + 1); // the rest + isInside = !isInside; // toggle + splitter = isInside ? '}' : '{'; // now look for next matching bracket + } + ret.push(str); + return ret.join(''); + } + + /** + * Get the magnitude of a number + */ + function getMagnitude(num) { + return math.pow(10, mathFloor(math.log(num) / math.LN10)); + } + + /** + * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 + * @param {Number} interval + * @param {Array} multiples + * @param {Number} magnitude + * @param {Object} options + */ + function normalizeTickInterval(interval, multiples, magnitude, allowDecimals, preventExceed) { + var normalized, + i, + retInterval = interval; + + // round to a tenfold of 1, 2, 2.5 or 5 + magnitude = pick(magnitude, 1); + normalized = interval / magnitude; + + // multiples for a linear scale + if (!multiples) { + multiples = [1, 2, 2.5, 5, 10]; + + // the allowDecimals option + if (allowDecimals === false) { + if (magnitude === 1) { + multiples = [1, 2, 5, 10]; + } else if (magnitude <= 0.1) { + multiples = [1 / magnitude]; + } + } + } + + // normalize the interval to the nearest multiple + for (i = 0; i < multiples.length; i++) { + retInterval = multiples[i]; + if ((preventExceed && retInterval * magnitude >= interval) || // only allow tick amounts smaller than natural + (!preventExceed && (normalized <= (multiples[i] + (multiples[i + 1] || multiples[i])) / 2))) { + break; + } + } + + // multiply back to the correct magnitude + retInterval *= magnitude; + + return retInterval; + } + + + /** + * Utility method that sorts an object array and keeping the order of equal items. + * ECMA script standard does not specify the behaviour when items are equal. + */ + function stableSort(arr, sortFunction) { + var length = arr.length, + sortValue, + i; + + // Add index to each item + for (i = 0; i < length; i++) { + arr[i].safeI = i; // stable sort index + } + + arr.sort(function (a, b) { + sortValue = sortFunction(a, b); + return sortValue === 0 ? a.safeI - b.safeI : sortValue; + }); + + // Remove index from items + for (i = 0; i < length; i++) { + delete arr[i].safeI; // stable sort index + } + } + + /** + * Non-recursive method to find the lowest member of an array. Math.min raises a maximum + * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This + * method is slightly slower, but safe. + */ + function arrayMin(data) { + var i = data.length, + min = data[0]; + + while (i--) { + if (data[i] < min) { + min = data[i]; + } + } + return min; + } + + /** + * Non-recursive method to find the lowest member of an array. Math.min raises a maximum + * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This + * method is slightly slower, but safe. + */ + function arrayMax(data) { + var i = data.length, + max = data[0]; + + while (i--) { + if (data[i] > max) { + max = data[i]; + } + } + return max; + } + + /** + * Utility method that destroys any SVGElement or VMLElement that are properties on the given object. + * It loops all properties and invokes destroy if there is a destroy method. The property is + * then delete'ed. + * @param {Object} The object to destroy properties on + * @param {Object} Exception, do not destroy this property, only delete it. + */ + function destroyObjectProperties(obj, except) { + var n; + for (n in obj) { + // If the object is non-null and destroy is defined + if (obj[n] && obj[n] !== except && obj[n].destroy) { + // Invoke the destroy + obj[n].destroy(); + } + + // Delete the property from the object. + delete obj[n]; + } + } + + + /** + * Discard an element by moving it to the bin and delete + * @param {Object} The HTML node to discard + */ + function discardElement(element) { + // create a garbage bin element, not part of the DOM + if (!garbageBin) { + garbageBin = createElement(DIV); + } + + // move the node and empty bin + if (element) { + garbageBin.appendChild(element); + } + garbageBin.innerHTML = ''; + } + + /** + * Fix JS round off float errors + * @param {Number} num + */ + function correctFloat(num, prec) { + return parseFloat( + num.toPrecision(prec || 14) + ); + } + + /** + * Set the global animation to either a given value, or fall back to the + * given chart's animation option + * @param {Object} animation + * @param {Object} chart + */ + function setAnimation(animation, chart) { + chart.renderer.globalAnimation = pick(animation, chart.animation); + } + + /** + * Get the animation in object form, where a disabled animation is always + * returned with duration: 0 + */ + function animObject(animation) { + return isObject(animation) ? merge(animation) : { duration: animation ? 500 : 0 }; + } + + /** + * The time unit lookup + */ + timeUnits = { + millisecond: 1, + second: 1000, + minute: 60000, + hour: 3600000, + day: 24 * 3600000, + week: 7 * 24 * 3600000, + month: 28 * 24 * 3600000, + year: 364 * 24 * 3600000 + }; + + + /** + * Format a number and return a string based on input settings + * @param {Number} number The input number to format + * @param {Number} decimals The amount of decimals + * @param {String} decimalPoint The decimal point, defaults to the one given in the lang options + * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options + */ + Highcharts.numberFormat = function (number, decimals, decimalPoint, thousandsSep) { + + number = +number || 0; + decimals = +decimals; + + var lang = defaultOptions.lang, + origDec = (number.toString().split('.')[1] || '').length, + decimalComponent, + strinteger, + thousands, + absNumber = Math.abs(number), + ret; + + if (decimals === -1) { + decimals = Math.min(origDec, 20); // Preserve decimals. Not huge numbers (#3793). + } else if (!isNumber(decimals)) { + decimals = 2; + } + + // A string containing the positive integer component of the number + strinteger = String(pInt(absNumber.toFixed(decimals))); + + // Leftover after grouping into thousands. Can be 0, 1 or 3. + thousands = strinteger.length > 3 ? strinteger.length % 3 : 0; + + // Language + decimalPoint = pick(decimalPoint, lang.decimalPoint); + thousandsSep = pick(thousandsSep, lang.thousandsSep); + + // Start building the return + ret = number < 0 ? '-' : ''; + + // Add the leftover after grouping into thousands. For example, in the number 42 000 000, + // this line adds 42. + ret += thousands ? strinteger.substr(0, thousands) + thousandsSep : ''; + + // Add the remaining thousands groups, joined by the thousands separator + ret += strinteger.substr(thousands).replace(/(\d{3})(?=\d)/g, '$1' + thousandsSep); + + // Add the decimal point and the decimal component + if (decimals) { + // Get the decimal component, and add power to avoid rounding errors with float numbers (#4573) + decimalComponent = Math.abs(absNumber - strinteger + Math.pow(10, -Math.max(decimals, origDec) - 1)); + ret += decimalPoint + decimalComponent.toFixed(decimals).slice(2); + } + + return ret; + }; + + /** + * Easing definition + * @param {Number} pos Current position, ranging from 0 to 1 + */ + Math.easeInOutSine = function (pos) { + return -0.5 * (Math.cos(Math.PI * pos) - 1); + }; + + /** + * Internal method to return CSS value for given element and property + */ + getStyle = function (el, prop) { + + var style; + + // For width and height, return the actual inner pixel size (#4913) + if (prop === 'width') { + return Math.min(el.offsetWidth, el.scrollWidth) - getStyle(el, 'padding-left') - getStyle(el, 'padding-right'); + } else if (prop === 'height') { + return Math.min(el.offsetHeight, el.scrollHeight) - getStyle(el, 'padding-top') - getStyle(el, 'padding-bottom'); + } + + // Otherwise, get the computed style + style = win.getComputedStyle(el, undefined); + return style && pInt(style.getPropertyValue(prop)); + }; + + /** + * Return the index of an item in an array, or -1 if not found + */ + inArray = function (item, arr) { + return arr.indexOf ? arr.indexOf(item) : [].indexOf.call(arr, item); + }; + + /** + * Filter an array + */ + grep = function (elements, callback) { + return [].filter.call(elements, callback); + }; + + /** + * Map an array + */ + map = function (arr, fn) { + var results = [], + i = 0, + len = arr.length; + + for (; i < len; i++) { + results[i] = fn.call(arr[i], arr[i], i, arr); + } + + return results; + }; + + /** + * Get the element's offset position, corrected by overflow:auto. + */ + offset = function (el) { + var docElem = doc.documentElement, + box = el.getBoundingClientRect(); + + return { + top: box.top + (win.pageYOffset || docElem.scrollTop) - (docElem.clientTop || 0), + left: box.left + (win.pageXOffset || docElem.scrollLeft) - (docElem.clientLeft || 0) + }; + }; + + /** + * Stop running animation. + * A possible extension to this would be to stop a single property, when + * we want to continue animating others. Then assign the prop to the timer + * in the Fx.run method, and check for the prop here. This would be an improvement + * in all cases where we stop the animation from .attr. Instead of stopping + * everything, we can just stop the actual attributes we're setting. + */ + stop = function (el) { + + var i = timers.length; + + // Remove timers related to this element (#4519) + while (i--) { + if (timers[i].elem === el) { + timers[i].stopped = true; // #4667 + } + } + }; + + /** + * Utility for iterating over an array. + * @param {Array} arr + * @param {Function} fn + */ + each = function (arr, fn) { // modern browsers + return Array.prototype.forEach.call(arr, fn); + }; + + /** + * Add an event listener + */ + addEvent = function (el, type, fn) { + + var events = el.hcEvents = el.hcEvents || {}; + + function wrappedFn(e) { + e.target = e.srcElement || win; // #2820 + fn.call(el, e); + } + + // Handle DOM events in modern browsers + if (el.addEventListener) { + el.addEventListener(type, fn, false); + + // Handle old IE implementation + } else if (el.attachEvent) { + + if (!el.hcEventsIE) { + el.hcEventsIE = {}; + } + + // Link wrapped fn with original fn, so we can get this in removeEvent + el.hcEventsIE[fn.toString()] = wrappedFn; + + el.attachEvent('on' + type, wrappedFn); + } + + if (!events[type]) { + events[type] = []; + } + + events[type].push(fn); + }; + + /** + * Remove event added with addEvent + */ + removeEvent = function (el, type, fn) { + + var events, + hcEvents = el.hcEvents, + index; + + function removeOneEvent(type, fn) { + if (el.removeEventListener) { + el.removeEventListener(type, fn, false); + } else if (el.attachEvent) { + fn = el.hcEventsIE[fn.toString()]; + el.detachEvent('on' + type, fn); + } + } + + function removeAllEvents() { + var types, + len, + n; + + if (!el.nodeName) { + return; // break on non-DOM events + } + + if (type) { + types = {}; + types[type] = true; + } else { + types = hcEvents; + } + + for (n in types) { + if (hcEvents[n]) { + len = hcEvents[n].length; + while (len--) { + removeOneEvent(n, hcEvents[n][len]); + } + } + } + } + + if (hcEvents) { + if (type) { + events = hcEvents[type] || []; + if (fn) { + index = inArray(fn, events); + if (index > -1) { + events.splice(index, 1); + hcEvents[type] = events; + } + removeOneEvent(type, fn); + + } else { + removeAllEvents(); + hcEvents[type] = []; + } + } else { + removeAllEvents(); + el.hcEvents = {}; + } + } + }; + + /** + * Fire an event on a custom object + */ + fireEvent = function (el, type, eventArguments, defaultFunction) { + var e, + hcEvents = el.hcEvents, + events, + len, + i, + fn; + + eventArguments = eventArguments || {}; + + if (doc.createEvent && (el.dispatchEvent || el.fireEvent)) { + e = doc.createEvent('Events'); + e.initEvent(type, true, true); + e.target = el; + + extend(e, eventArguments); + + if (el.dispatchEvent) { + el.dispatchEvent(e); + } else { + el.fireEvent(type, e); + } + + } else if (hcEvents) { + + events = hcEvents[type] || []; + len = events.length; + + // Attach a simple preventDefault function to skip default handler if called. + // The built-in defaultPrevented property is not overwritable (#5112) + if (!eventArguments.preventDefault) { + eventArguments.preventDefault = function () { + eventArguments.defaultPrevented = true; + }; + } + + eventArguments.target = el; + + // If the type is not set, we're running a custom event (#2297). If it is set, + // we're running a browser event, and setting it will cause en error in + // IE8 (#2465). + if (!eventArguments.type) { + eventArguments.type = type; + } + + for (i = 0; i < len; i++) { + fn = events[i]; + + // If the event handler return false, prevent the default handler from executing + if (fn && fn.call(el, eventArguments) === false) { + eventArguments.preventDefault(); + } + } + } + + // Run the default if not prevented + if (defaultFunction && !eventArguments.defaultPrevented) { + defaultFunction(eventArguments); + } + }; + + /** + * The global animate method, which uses Fx to create individual animators. + */ + animate = function (el, params, opt) { + var start, + unit = '', + end, + fx, + args, + prop; + + if (!isObject(opt)) { // Number or undefined/null + args = arguments; + opt = { + duration: args[2], + easing: args[3], + complete: args[4] + }; + } + if (!isNumber(opt.duration)) { + opt.duration = 400; + } + opt.easing = typeof opt.easing === 'function' ? opt.easing : (Math[opt.easing] || Math.easeInOutSine); + opt.curAnim = merge(params); + + for (prop in params) { + fx = new Fx(el, opt, prop); + end = null; + + if (prop === 'd') { + fx.paths = fx.initPath( + el, + el.d, + params.d + ); + fx.toD = params.d; + start = 0; + end = 1; + } else if (el.attr) { + start = el.attr(prop); + } else { + start = parseFloat(getStyle(el, prop)) || 0; + if (prop !== 'opacity') { + unit = 'px'; + } + } + + if (!end) { + end = params[prop]; + } + if (end.match && end.match('px')) { + end = end.replace(/px/g, ''); // #4351 + } + fx.run(start, end, unit); + } + }; + + /** + * Register Highcharts as a plugin in jQuery + */ + if (win.jQuery) { + win.jQuery.fn.highcharts = function () { + var args = [].slice.call(arguments); + + if (this[0]) { // this[0] is the renderTo div + + // Create the chart + if (args[0]) { + new Highcharts[ // eslint-disable-line no-new + isString(args[0]) ? args.shift() : 'Chart' // Constructor defaults to Chart + ](this[0], args[0], args[1]); + return this; + } + + // When called without parameters or with the return argument, return an existing chart + return charts[attr(this[0], 'data-highcharts-chart')]; + } + }; + } + + + /** + * Compatibility section to add support for legacy IE. This can be removed if old IE + * support is not needed. + */ + if (doc && !doc.defaultView) { + getStyle = function (el, prop) { + var val, + alias = { width: 'clientWidth', height: 'clientHeight' }[prop]; + + if (el.style[prop]) { + return pInt(el.style[prop]); + } + if (prop === 'opacity') { + prop = 'filter'; + } + + // Getting the rendered width and height + if (alias) { + el.style.zoom = 1; + return Math.max(el[alias] - 2 * getStyle(el, 'padding'), 0); + } + + val = el.currentStyle[prop.replace(/\-(\w)/g, function (a, b) { + return b.toUpperCase(); + })]; + if (prop === 'filter') { + val = val.replace( + /alpha\(opacity=([0-9]+)\)/, + function (a, b) { + return b / 100; + } + ); + } + + return val === '' ? 1 : pInt(val); + }; + } + + if (!Array.prototype.forEach) { + each = function (arr, fn) { // legacy + var i = 0, + len = arr.length; + for (; i < len; i++) { + if (fn.call(arr[i], arr[i], i, arr) === false) { + return i; + } + } + }; + } + + if (!Array.prototype.indexOf) { + inArray = function (item, arr) { + var len, + i = 0; + + if (arr) { + len = arr.length; + + for (; i < len; i++) { + if (arr[i] === item) { + return i; + } + } + } + + return -1; + }; + } + + if (!Array.prototype.filter) { + grep = function (elements, fn) { + var ret = [], + i = 0, + length = elements.length; + + for (; i < length; i++) { + if (fn(elements[i], i)) { + ret.push(elements[i]); + } + } + + return ret; + }; + } + + //--- End compatibility section --- + + // Expose utilities + Highcharts.Fx = Fx; + Highcharts.inArray = inArray; + Highcharts.each = each; + Highcharts.grep = grep; + Highcharts.offset = offset; + Highcharts.map = map; + Highcharts.addEvent = addEvent; + Highcharts.removeEvent = removeEvent; + Highcharts.fireEvent = fireEvent; + Highcharts.animate = animate; + Highcharts.animObject = animObject; + Highcharts.stop = stop; + + /* **************************************************************************** + * Handle the options * + *****************************************************************************/ + defaultOptions = { + colors: ['#7cb5ec', '#434348', '#90ed7d', '#f7a35c', + '#8085e9', '#f15c80', '#e4d354', '#2b908f', '#f45b5b', '#91e8e1'], + symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'], + lang: { + loading: 'Loading...', + months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December'], + shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + // invalidDate: '', + decimalPoint: '.', + numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], // SI prefixes used in axis labels + resetZoom: 'Reset zoom', + resetZoomTitle: 'Reset zoom level 1:1', + thousandsSep: ' ' + }, + global: { + useUTC: true, + //timezoneOffset: 0, + canvasToolsURL: 'http://code.highcharts.com/modules/canvas-tools.js', + VMLRadialGradientURL: 'http://code.highcharts.com/4.2.5-modified/gfx/vml-radial-gradient.png' + }, + chart: { + //animation: true, + //alignTicks: false, + //reflow: true, + //className: null, + //events: { load, selection }, + //margin: [null], + //marginTop: null, + //marginRight: null, + //marginBottom: null, + //marginLeft: null, + borderColor: '#4572A7', + //borderWidth: 0, + borderRadius: 0, + defaultSeriesType: 'line', + ignoreHiddenSeries: true, + //inverted: false, + //shadow: false, + spacing: [10, 10, 15, 10], + //spacingTop: 10, + //spacingRight: 10, + //spacingBottom: 15, + //spacingLeft: 10, + //style: { + // fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font + // fontSize: '12px' + //}, + backgroundColor: '#FFFFFF', + //plotBackgroundColor: null, + plotBorderColor: '#C0C0C0', + //plotBorderWidth: 0, + //plotShadow: false, + //zoomType: '' + resetZoomButton: { + theme: { + zIndex: 20 + }, + position: { + align: 'right', + x: -10, + //verticalAlign: 'top', + y: 10 + } + // relativeTo: 'plot' + } + }, + title: { + text: 'Chart title', + align: 'center', + // floating: false, + margin: 15, + // x: 0, + // verticalAlign: 'top', + // y: null, + style: { + color: '#333333', + fontSize: '18px' + }, + widthAdjust: -44 + + }, + subtitle: { + text: '', + align: 'center', + // floating: false + // x: 0, + // verticalAlign: 'top', + // y: null, + style: { + color: '#555555' + }, + widthAdjust: -44 + }, + + plotOptions: { + line: { // base series options + allowPointSelect: false, + showCheckbox: false, + animation: { + duration: 1000 + }, + //connectNulls: false, + //cursor: 'default', + //clip: true, + //dashStyle: null, + //enableMouseTracking: true, + events: {}, + //legendIndex: 0, + //linecap: 'round', + lineWidth: 2, + //shadow: false, + // stacking: null, + marker: { + //enabled: true, + //symbol: null, + lineWidth: 0, + radius: 4, + lineColor: '#FFFFFF', + //fillColor: null, + states: { // states for a single point + hover: { + enabled: true, + lineWidthPlus: 1, + radiusPlus: 2 + }, + select: { + fillColor: '#FFFFFF', + lineColor: '#000000', + lineWidth: 2 + } + } + }, + point: { + events: {} + }, + dataLabels: { + align: 'center', + // defer: true, + // enabled: false, + formatter: function () { + return this.y === null ? '' : Highcharts.numberFormat(this.y, -1); + }, + style: { + color: 'contrast', + fontSize: '11px', + fontWeight: 'bold', + textShadow: '0 0 6px contrast, 0 0 3px contrast' + }, + verticalAlign: 'bottom', // above singular point + x: 0, + y: 0, + // backgroundColor: undefined, + // borderColor: undefined, + // borderRadius: undefined, + // borderWidth: undefined, + padding: 5 + // shadow: false + }, + cropThreshold: 300, // draw points outside the plot area when the number of points is less than this + pointRange: 0, + //pointStart: 0, + //pointInterval: 1, + //showInLegend: null, // auto: true for standalone series, false for linked series + softThreshold: true, + states: { // states for the entire series + hover: { + //enabled: false, + lineWidthPlus: 1, + marker: { + // lineWidth: base + 1, + // radius: base + 1 + }, + halo: { + size: 10, + opacity: 0.25 + } + }, + select: { + marker: {} + } + }, + stickyTracking: true, + //tooltip: { + //pointFormat: '\u25CF {series.name}: {point.y}' + //valueDecimals: null, + //xDateFormat: '%A, %b %e, %Y', + //valuePrefix: '', + //ySuffix: '' + //} + turboThreshold: 1000 + // zIndex: null + } + }, + labels: { + //items: [], + style: { + //font: defaultFont, + position: ABSOLUTE, + color: '#3E576F' + } + }, + legend: { + enabled: true, + align: 'center', + //floating: false, + layout: 'horizontal', + labelFormatter: function () { + return this.name; + }, + //borderWidth: 0, + borderColor: '#909090', + borderRadius: 0, + navigation: { + // animation: true, + activeColor: '#274b6d', + // arrowSize: 12 + inactiveColor: '#CCC' + // style: {} // text styles + }, + // margin: 20, + // reversed: false, + shadow: false, + // backgroundColor: null, + /*style: { + padding: '5px' + },*/ + itemStyle: { + color: '#333333', + fontSize: '12px', + fontWeight: 'bold' + }, + itemHoverStyle: { + //cursor: 'pointer', removed as of #601 + color: '#000' + }, + itemHiddenStyle: { + color: '#CCC' + }, + itemCheckboxStyle: { + position: ABSOLUTE, + width: '13px', // for IE precision + height: '13px' + }, + // itemWidth: undefined, + // symbolRadius: 0, + // symbolWidth: 16, + symbolPadding: 5, + verticalAlign: 'bottom', + // width: undefined, + x: 0, + y: 0, + title: { + //text: null, + style: { + fontWeight: 'bold' + } + } + }, + + loading: { + // hideDuration: 100, + labelStyle: { + fontWeight: 'bold', + position: RELATIVE, + top: '45%' + }, + // showDuration: 0, + style: { + position: ABSOLUTE, + backgroundColor: 'white', + opacity: 0.5, + textAlign: 'center' + } + }, + + tooltip: { + enabled: true, + animation: hasSVG, + //crosshairs: null, + backgroundColor: 'rgba(249, 249, 249, .85)', + borderWidth: 1, + borderRadius: 3, + dateTimeLabelFormats: { + millisecond: '%A, %b %e, %H:%M:%S.%L', + second: '%A, %b %e, %H:%M:%S', + minute: '%A, %b %e, %H:%M', + hour: '%A, %b %e, %H:%M', + day: '%A, %b %e, %Y', + week: 'Week from %A, %b %e, %Y', + month: '%B %Y', + year: '%Y' + }, + footerFormat: '', + //formatter: defaultFormatter, + headerFormat: '{point.key}
', + pointFormat: '\u25CF {series.name}: {point.y}
', + shadow: true, + //shape: 'callout', + //shared: false, + snap: isTouchDevice ? 25 : 10, + style: { + color: '#333333', + cursor: 'default', + fontSize: '12px', + padding: '8px', + pointerEvents: 'none', // #1686 http://caniuse.com/#feat=pointer-events + whiteSpace: 'nowrap' + } + //xDateFormat: '%A, %b %e, %Y', + //valueDecimals: null, + //valuePrefix: '', + //valueSuffix: '' + }, + + credits: { + enabled: true, + text: 'Highcharts.com', + href: 'http://www.highcharts.com', + position: { + align: 'right', + x: -10, + verticalAlign: 'bottom', + y: -5 + }, + style: { + cursor: 'pointer', + color: '#909090', + fontSize: '9px' + } + } + }; + + + + /** + * Set the time methods globally based on the useUTC option. Time method can be either + * local time or UTC (default). + */ + function setTimeMethods() { + var globalOptions = defaultOptions.global, + useUTC = globalOptions.useUTC, + GET = useUTC ? 'getUTC' : 'get', + SET = useUTC ? 'setUTC' : 'set'; + + + Date = globalOptions.Date || win.Date; + timezoneOffset = useUTC && globalOptions.timezoneOffset; + getTimezoneOffset = useUTC && globalOptions.getTimezoneOffset; + makeTime = function (year, month, date, hours, minutes, seconds) { + var d; + if (useUTC) { + d = Date.UTC.apply(0, arguments); + d += getTZOffset(d); + } else { + d = new Date( + year, + month, + pick(date, 1), + pick(hours, 0), + pick(minutes, 0), + pick(seconds, 0) + ).getTime(); + } + return d; + }; + getMinutes = GET + 'Minutes'; + getHours = GET + 'Hours'; + getDay = GET + 'Day'; + getDate = GET + 'Date'; + getMonth = GET + 'Month'; + getFullYear = GET + 'FullYear'; + setMilliseconds = SET + 'Milliseconds'; + setSeconds = SET + 'Seconds'; + setMinutes = SET + 'Minutes'; + setHours = SET + 'Hours'; + setDate = SET + 'Date'; + setMonth = SET + 'Month'; + setFullYear = SET + 'FullYear'; + + } + + /** + * Merge the default options with custom options and return the new options structure + * @param {Object} options The new custom options + */ + function setOptions(options) { + + // Copy in the default options + defaultOptions = merge(true, defaultOptions, options); + + // Apply UTC + setTimeMethods(); + + return defaultOptions; + } + + /** + * Get the updated default options. Until 3.0.7, merely exposing defaultOptions for outside modules + * wasn't enough because the setOptions method created a new object. + */ + function getOptions() { + return defaultOptions; + } + + + + + + + // Series defaults + var defaultPlotOptions = defaultOptions.plotOptions, + defaultSeriesOptions = defaultPlotOptions.line; + + // set the default time methods + setTimeMethods(); + + + /** + * Handle color operations. The object methods are chainable. + * @param {String} input The input color in either rbga or hex format + */ + function Color(input) { + // Backwards compatibility, allow instanciation without new + if (!(this instanceof Color)) { + return new Color(input); + } + // Initialize + this.init(input); + } + Color.prototype = { + + // Collection of parsers. This can be extended from the outside by pushing parsers + // to Highcharts.Colors.prototype.parsers. + parsers: [{ + // RGBA color + regex: /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/, + parse: function (result) { + return [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)]; + } + }, { + // HEX color + regex: /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/, + parse: function (result) { + return [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1]; + } + }, { + // RGB color + regex: /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/, + parse: function (result) { + return [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1]; + } + }], + + /** + * Parse the input color to rgba array + * @param {String} input + */ + init: function (input) { + var result, + rgba, + i, + parser; + + this.input = input; + + // Gradients + if (input && input.stops) { + this.stops = map(input.stops, function (stop) { + return new Color(stop[1]); + }); + + // Solid colors + } else { + i = this.parsers.length; + while (i-- && !rgba) { + parser = this.parsers[i]; + result = parser.regex.exec(input); + if (result) { + rgba = parser.parse(result); + } + } + } + this.rgba = rgba || []; + }, + + /** + * Return the color a specified format + * @param {String} format + */ + get: function (format) { + var input = this.input, + rgba = this.rgba, + ret; + + if (this.stops) { + ret = merge(input); + ret.stops = [].concat(ret.stops); + each(this.stops, function (stop, i) { + ret.stops[i] = [ret.stops[i][0], stop.get(format)]; + }); + + // it's NaN if gradient colors on a column chart + } else if (rgba && isNumber(rgba[0])) { + if (format === 'rgb' || (!format && rgba[3] === 1)) { + ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')'; + } else if (format === 'a') { + ret = rgba[3]; + } else { + ret = 'rgba(' + rgba.join(',') + ')'; + } + } else { + ret = input; + } + return ret; + }, + + /** + * Brighten the color + * @param {Number} alpha + */ + brighten: function (alpha) { + var i, + rgba = this.rgba; + + if (this.stops) { + each(this.stops, function (stop) { + stop.brighten(alpha); + }); + + } else if (isNumber(alpha) && alpha !== 0) { + for (i = 0; i < 3; i++) { + rgba[i] += pInt(alpha * 255); + + if (rgba[i] < 0) { + rgba[i] = 0; + } + if (rgba[i] > 255) { + rgba[i] = 255; + } + } + } + return this; + }, + + /** + * Set the color's opacity to a given alpha value + * @param {Number} alpha + */ + setOpacity: function (alpha) { + this.rgba[3] = alpha; + return this; + } + }; + + + /** + * A wrapper object for SVG elements + */ + function SVGElement() {} + + SVGElement.prototype = { + + // Default base for animation + opacity: 1, + // For labels, these CSS properties are applied to the node directly + textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily', 'fontStyle', 'color', + 'lineHeight', 'width', 'textDecoration', 'textOverflow', 'textShadow'], + + /** + * Initialize the SVG renderer + * @param {Object} renderer + * @param {String} nodeName + */ + init: function (renderer, nodeName) { + var wrapper = this; + wrapper.element = nodeName === 'span' ? + createElement(nodeName) : + doc.createElementNS(SVG_NS, nodeName); + wrapper.renderer = renderer; + }, + + /** + * Animate a given attribute + * @param {Object} params + * @param {Number} options Options include duration, easing, step and complete + * @param {Function} complete Function to perform at the end of animation + */ + animate: function (params, options, complete) { + var animOptions = pick(options, this.renderer.globalAnimation, true); + stop(this); // stop regardless of animation actually running, or reverting to .attr (#607) + if (animOptions) { + if (complete) { // allows using a callback with the global animation without overwriting it + animOptions.complete = complete; + } + animate(this, params, animOptions); + } else { + this.attr(params, null, complete); + } + return this; + }, + + /** + * Build an SVG gradient out of a common JavaScript configuration object + */ + colorGradient: function (color, prop, elem) { + var renderer = this.renderer, + colorObject, + gradName, + gradAttr, + radAttr, + gradients, + gradientObject, + stops, + stopColor, + stopOpacity, + radialReference, + n, + id, + key = [], + value; + + // Apply linear or radial gradients + if (color.linearGradient) { + gradName = 'linearGradient'; + } else if (color.radialGradient) { + gradName = 'radialGradient'; + } + + if (gradName) { + gradAttr = color[gradName]; + gradients = renderer.gradients; + stops = color.stops; + radialReference = elem.radialReference; + + // Keep < 2.2 kompatibility + if (isArray(gradAttr)) { + color[gradName] = gradAttr = { + x1: gradAttr[0], + y1: gradAttr[1], + x2: gradAttr[2], + y2: gradAttr[3], + gradientUnits: 'userSpaceOnUse' + }; + } + + // Correct the radial gradient for the radial reference system + if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) { + radAttr = gradAttr; // Save the radial attributes for updating + gradAttr = merge(gradAttr, + renderer.getRadialAttr(radialReference, radAttr), + { gradientUnits: 'userSpaceOnUse' } + ); + } + + // Build the unique key to detect whether we need to create a new element (#1282) + for (n in gradAttr) { + if (n !== 'id') { + key.push(n, gradAttr[n]); + } + } + for (n in stops) { + key.push(stops[n]); + } + key = key.join(','); + + // Check if a gradient object with the same config object is created within this renderer + if (gradients[key]) { + id = gradients[key].attr('id'); + + } else { + + // Set the id and create the element + gradAttr.id = id = PREFIX + idCounter++; + gradients[key] = gradientObject = renderer.createElement(gradName) + .attr(gradAttr) + .add(renderer.defs); + + gradientObject.radAttr = radAttr; + + // The gradient needs to keep a list of stops to be able to destroy them + gradientObject.stops = []; + each(stops, function (stop) { + var stopObject; + if (stop[1].indexOf('rgba') === 0) { + 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); + }); + } + + // Set the reference to the gradient object + value = 'url(' + renderer.url + '#' + id + ')'; + elem.setAttribute(prop, value); + elem.gradient = key; + + // Allow the color to be concatenated into tooltips formatters etc. (#2995) + color.toString = function () { + return value; + }; + } + }, + + /** + * Apply a polyfill to the text-stroke CSS property, by copying the text element + * and apply strokes to the copy. + * + * Contrast checks at http://jsfiddle.net/highcharts/43soe9m1/2/ + */ + applyTextShadow: function (textShadow) { + var elem = this.element, + tspans, + hasContrast = textShadow.indexOf('contrast') !== -1, + styles = {}, + forExport = this.renderer.forExport, + // IE10 and IE11 report textShadow in elem.style even though it doesn't work. Check + // this again with new IE release. In exports, the rendering is passed to PhantomJS. + supports = forExport || (elem.style.textShadow !== UNDEFINED && !isMS); + + // When the text shadow is set to contrast, use dark stroke for light text and vice versa + if (hasContrast) { + styles.textShadow = textShadow = textShadow.replace(/contrast/g, this.renderer.getContrast(elem.style.fill)); + } + + // Safari with retina displays as well as PhantomJS bug (#3974). Firefox does not tolerate this, + // it removes the text shadows. + if (isWebKit || forExport) { + styles.textRendering = 'geometricPrecision'; + } + + /* Selective side-by-side testing in supported browser (http://jsfiddle.net/highcharts/73L1ptrh/) + if (elem.textContent.indexOf('2.') === 0) { + elem.style['text-shadow'] = 'none'; + supports = false; + } + // */ + + // No reason to polyfill, we've got native support + if (supports) { + this.css(styles); // Apply altered textShadow or textRendering workaround + } else { + + this.fakeTS = true; // Fake text shadow + + // In order to get the right y position of the clones, + // copy over the y setter + this.ySetter = this.xSetter; + + tspans = [].slice.call(elem.getElementsByTagName('tspan')); + each(textShadow.split(/\s?,\s?/g), function (textShadow) { + var firstChild = elem.firstChild, + color, + strokeWidth; + + textShadow = textShadow.split(' '); + color = textShadow[textShadow.length - 1]; + + // Approximately tune the settings to the text-shadow behaviour + strokeWidth = textShadow[textShadow.length - 2]; + + if (strokeWidth) { + each(tspans, function (tspan, y) { + var clone; + + // Let the first line start at the correct X position + if (y === 0) { + tspan.setAttribute('x', elem.getAttribute('x')); + y = elem.getAttribute('y'); + tspan.setAttribute('y', y || 0); + if (y === null) { + elem.setAttribute('y', 0); + } + } + + // Create the clone and apply shadow properties + clone = tspan.cloneNode(1); + attr(clone, { + 'class': PREFIX + 'text-shadow', + 'fill': color, + 'stroke': color, + 'stroke-opacity': 1 / mathMax(pInt(strokeWidth), 3), + 'stroke-width': strokeWidth, + 'stroke-linejoin': 'round' + }); + elem.insertBefore(clone, firstChild); + }); + } + }); + } + }, + + /** + * Set or get a given attribute + * @param {Object|String} hash + * @param {Mixed|Undefined} val + */ + attr: function (hash, val, complete) { + var key, + value, + element = this.element, + hasSetSymbolSize, + ret = this, + skipAttr, + setter; + + // single key-value pair + if (typeof hash === 'string' && val !== UNDEFINED) { + key = hash; + hash = {}; + hash[key] = val; + } + + // used as a getter: first argument is a string, second is undefined + if (typeof hash === 'string') { + ret = (this[hash + 'Getter'] || this._defaultGetter).call(this, hash, element); + + // setter + } else { + + for (key in hash) { + value = hash[key]; + skipAttr = false; + + + + if (this.symbolName && /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(key)) { + if (!hasSetSymbolSize) { + this.symbolAttr(hash); + hasSetSymbolSize = true; + } + skipAttr = true; + } + + if (this.rotation && (key === 'x' || key === 'y')) { + this.doTransform = true; + } + + if (!skipAttr) { + setter = this[key + 'Setter'] || this._defaultSetter; + setter.call(this, value, key, element); + + // Let the shadow follow the main element + if (this.shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) { + this.updateShadows(key, value, setter); + } + } + } + + // Update transform. Do this outside the loop to prevent redundant updating for batch setting + // of attributes. + if (this.doTransform) { + this.updateTransform(); + this.doTransform = false; + } + + } + + // In accordance with animate, run a complete callback + if (complete) { + complete(); + } + + return ret; + }, + + /** + * Update the shadow elements with new attributes + * @param {String} key The attribute name + * @param {String|Number} value The value of the attribute + * @param {Function} setter The setter function, inherited from the parent wrapper + * @returns {undefined} + */ + updateShadows: function (key, value, setter) { + var shadows = this.shadows, + i = shadows.length; + + while (i--) { + setter.call( + shadows[i], + key === 'height' ? + Math.max(value - (shadows[i].cutHeight || 0), 0) : + key === 'd' ? this.d : value, + key, + shadows[i] + ); + } + }, + + /** + * Add a class name to an element + */ + addClass: function (className) { + var element = this.element, + currentClassName = attr(element, 'class') || ''; + + if (currentClassName.indexOf(className) === -1) { + attr(element, 'class', currentClassName + ' ' + className); + } + return this; + }, + /* hasClass and removeClass are not (yet) needed + hasClass: function (className) { + return attr(this.element, 'class').indexOf(className) !== -1; + }, + removeClass: function (className) { + attr(this.element, 'class', attr(this.element, 'class').replace(className, '')); + return this; + }, + */ + + /** + * If one of the symbol size affecting parameters are changed, + * check all the others only once for each call to an element's + * .attr() method + * @param {Object} hash + */ + symbolAttr: function (hash) { + var wrapper = this; + + each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function (key) { + wrapper[key] = pick(hash[key], wrapper[key]); + }); + + wrapper.attr({ + d: wrapper.renderer.symbols[wrapper.symbolName]( + wrapper.x, + wrapper.y, + wrapper.width, + wrapper.height, + wrapper + ) + }); + }, + + /** + * Apply a clipping path to this object + * @param {String} id + */ + clip: function (clipRect) { + return this.attr('clip-path', clipRect ? 'url(' + this.renderer.url + '#' + clipRect.id + ')' : NONE); + }, + + /** + * Calculate the coordinates needed for drawing a rectangle crisply and return the + * calculated attributes + * @param {Number} strokeWidth + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + crisp: function (rect) { + + var wrapper = this, + key, + attribs = {}, + normalizer, + strokeWidth = wrapper.strokeWidth || 0; + + normalizer = mathRound(strokeWidth) % 2 / 2; // mathRound because strokeWidth can sometimes have roundoff errors + + // normalize for crisp edges + rect.x = mathFloor(rect.x || wrapper.x || 0) + normalizer; + rect.y = mathFloor(rect.y || wrapper.y || 0) + normalizer; + rect.width = mathFloor((rect.width || wrapper.width || 0) - 2 * normalizer); + rect.height = mathFloor((rect.height || wrapper.height || 0) - 2 * normalizer); + rect.strokeWidth = strokeWidth; + + for (key in rect) { + if (wrapper[key] !== rect[key]) { // only set attribute if changed + wrapper[key] = attribs[key] = rect[key]; + } + } + + return attribs; + }, + + /** + * Set styles for the element + * @param {Object} styles + */ + css: function (styles) { + var elemWrapper = this, + oldStyles = elemWrapper.styles, + newStyles = {}, + elem = elemWrapper.element, + textWidth, + n, + serializedCss = '', + hyphenate, + hasNew = !oldStyles; + + // convert legacy + if (styles && styles.color) { + styles.fill = styles.color; + } + + // Filter out existing styles to increase performance (#2640) + if (oldStyles) { + for (n in styles) { + if (styles[n] !== oldStyles[n]) { + newStyles[n] = styles[n]; + hasNew = true; + } + } + } + if (hasNew) { + textWidth = elemWrapper.textWidth = + (styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width)) || + elemWrapper.textWidth; // #3501 + + // Merge the new styles with the old ones + if (oldStyles) { + styles = extend( + oldStyles, + newStyles + ); + } + + // store object + elemWrapper.styles = styles; + + if (textWidth && (useCanVG || (!hasSVG && elemWrapper.renderer.forExport))) { + delete styles.width; + } + + // serialize and set style attribute + if (isMS && !hasSVG) { + css(elemWrapper.element, styles); + } else { + hyphenate = function (a, b) { + return '-' + b.toLowerCase(); + }; + for (n in styles) { + serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';'; + } + attr(elem, 'style', serializedCss); // #1881 + } + + + // re-build text + if (textWidth && elemWrapper.added) { + elemWrapper.renderer.buildText(elemWrapper); + } + } + + return elemWrapper; + }, + + /** + * Add an event listener + * @param {String} eventType + * @param {Function} handler + */ + on: function (eventType, handler) { + var svgElement = this, + element = svgElement.element; + + // touch + if (hasTouch && eventType === 'click') { + element.ontouchstart = function (e) { + svgElement.touchEventFired = Date.now(); + e.preventDefault(); + handler.call(element, e); + }; + element.onclick = function (e) { + if (userAgent.indexOf('Android') === -1 || Date.now() - (svgElement.touchEventFired || 0) > 1100) { // #2269 + handler.call(element, e); + } + }; + } else { + // simplest possible event model for internal use + element['on' + eventType] = handler; + } + return this; + }, + + /** + * Set the coordinates needed to draw a consistent radial gradient across + * pie slices regardless of positioning inside the chart. The format is + * [centerX, centerY, diameter] in pixels. + */ + setRadialReference: function (coordinates) { + var existingGradient = this.renderer.gradients[this.element.gradient]; + + this.element.radialReference = coordinates; + + // On redrawing objects with an existing gradient, the gradient needs + // to be repositioned (#3801) + if (existingGradient && existingGradient.radAttr) { + existingGradient.animate( + this.renderer.getRadialAttr( + coordinates, + existingGradient.radAttr + ) + ); + } + + return this; + }, + + /** + * Move an object and its children by x and y values + * @param {Number} x + * @param {Number} y + */ + translate: function (x, y) { + return this.attr({ + translateX: x, + translateY: y + }); + }, + + /** + * Invert a group, rotate and flip + */ + invert: function () { + var wrapper = this; + wrapper.inverted = true; + wrapper.updateTransform(); + return wrapper; + }, + + /** + * Private method to update the transform attribute based on internal + * properties + */ + updateTransform: function () { + var wrapper = this, + translateX = wrapper.translateX || 0, + translateY = wrapper.translateY || 0, + scaleX = wrapper.scaleX, + scaleY = wrapper.scaleY, + inverted = wrapper.inverted, + rotation = wrapper.rotation, + element = wrapper.element, + transform; + + // flipping affects translate as adjustment for flipping around the group's axis + if (inverted) { + translateX += wrapper.attr('width'); + translateY += wrapper.attr('height'); + } + + // Apply translate. Nearly all transformed elements have translation, so instead + // of checking for translate = 0, do it always (#1767, #1846). + transform = ['translate(' + translateX + ',' + translateY + ')']; + + // apply rotation + if (inverted) { + transform.push('rotate(90) scale(-1,1)'); + } else if (rotation) { // text rotation + transform.push('rotate(' + rotation + ' ' + (element.getAttribute('x') || 0) + ' ' + (element.getAttribute('y') || 0) + ')'); + + // Delete bBox memo when the rotation changes + //delete wrapper.bBox; + } + + // apply scale + if (defined(scaleX) || defined(scaleY)) { + transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')'); + } + + if (transform.length) { + element.setAttribute('transform', transform.join(' ')); + } + }, + /** + * Bring the element to the front + */ + toFront: function () { + var element = this.element; + element.parentNode.appendChild(element); + return this; + }, + + + /** + * Break down alignment options like align, verticalAlign, x and y + * to x and y relative to the chart. + * + * @param {Object} alignOptions + * @param {Boolean} alignByTranslate + * @param {String[Object} box The box to align to, needs a width and height. When the + * box is a string, it refers to an object in the Renderer. For example, when + * box is 'spacingBox', it refers to Renderer.spacingBox which holds width, height + * x and y properties. + * + */ + align: function (alignOptions, alignByTranslate, box) { + var align, + vAlign, + x, + y, + attribs = {}, + alignTo, + renderer = this.renderer, + alignedObjects = renderer.alignedObjects; + + // First call on instanciate + if (alignOptions) { + this.alignOptions = alignOptions; + this.alignByTranslate = alignByTranslate; + if (!box || isString(box)) { // boxes other than renderer handle this internally + this.alignTo = alignTo = box || 'renderer'; + erase(alignedObjects, this); // prevent duplicates, like legendGroup after resize + alignedObjects.push(this); + box = null; // reassign it below + } + + // When called on resize, no arguments are supplied + } else { + alignOptions = this.alignOptions; + alignByTranslate = this.alignByTranslate; + alignTo = this.alignTo; + } + + box = pick(box, renderer[alignTo], renderer); + + // Assign variables + align = alignOptions.align; + vAlign = alignOptions.verticalAlign; + x = (box.x || 0) + (alignOptions.x || 0); // default: left align + y = (box.y || 0) + (alignOptions.y || 0); // default: top align + + // Align + if (align === 'right' || align === 'center') { + x += (box.width - (alignOptions.width || 0)) / + { right: 1, center: 2 }[align]; + } + attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x); + + + // Vertical align + if (vAlign === 'bottom' || vAlign === 'middle') { + y += (box.height - (alignOptions.height || 0)) / + ({ bottom: 1, middle: 2 }[vAlign] || 1); + + } + attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y); + + // Animate only if already placed + this[this.placed ? 'animate' : 'attr'](attribs); + this.placed = true; + this.alignAttr = attribs; + + return this; + }, + + /** + * Get the bounding box (width, height, x and y) for the element + */ + getBBox: function (reload, rot) { + var wrapper = this, + bBox, // = wrapper.bBox, + renderer = wrapper.renderer, + width, + height, + rotation, + rad, + element = wrapper.element, + styles = wrapper.styles, + textStr = wrapper.textStr, + textShadow, + elemStyle = element.style, + toggleTextShadowShim, + cache = renderer.cache, + cacheKeys = renderer.cacheKeys, + cacheKey; + + rotation = pick(rot, wrapper.rotation); + rad = rotation * deg2rad; + + if (textStr !== UNDEFINED) { + + // Properties that affect bounding box + cacheKey = ['', rotation || 0, styles && styles.fontSize, element.style.width].join(','); + + // Since numbers are monospaced, and numerical labels appear a lot in a chart, + // we assume that a label of n characters has the same bounding box as others + // of the same length. + if (textStr === '' || numRegex.test(textStr)) { + cacheKey = 'num:' + textStr.toString().length + cacheKey; + + // Caching all strings reduces rendering time by 4-5%. + } else { + cacheKey = textStr + cacheKey; + } + } + + if (cacheKey && !reload) { + bBox = cache[cacheKey]; + } + + // No cache found + if (!bBox) { + + // SVG elements + if (element.namespaceURI === SVG_NS || renderer.forExport) { + try { // Fails in Firefox if the container has display: none. + + // When the text shadow shim is used, we need to hide the fake shadows + // to get the correct bounding box (#3872) + toggleTextShadowShim = this.fakeTS && function (display) { + each(element.querySelectorAll('.' + PREFIX + 'text-shadow'), function (tspan) { + tspan.style.display = display; + }); + }; + + // Workaround for #3842, Firefox reporting wrong bounding box for shadows + if (isFirefox && elemStyle.textShadow) { + textShadow = elemStyle.textShadow; + elemStyle.textShadow = ''; + } else if (toggleTextShadowShim) { + toggleTextShadowShim(NONE); + } + + bBox = element.getBBox ? + // SVG: use extend because IE9 is not allowed to change width and height in case + // of rotation (below) + extend({}, element.getBBox()) : + // Canvas renderer and legacy IE in export mode + { + width: element.offsetWidth, + height: element.offsetHeight + }; + + // #3842 + if (textShadow) { + elemStyle.textShadow = textShadow; + } else if (toggleTextShadowShim) { + toggleTextShadowShim(''); + } + } catch (e) {} + + // If the bBox is not set, the try-catch block above failed. The other condition + // is for Opera that returns a width of -Infinity on hidden elements. + if (!bBox || bBox.width < 0) { + bBox = { width: 0, height: 0 }; + } + + + // VML Renderer or useHTML within SVG + } else { + + bBox = wrapper.htmlGetBBox(); + + } + + // True SVG elements as well as HTML elements in modern browsers using the .useHTML option + // need to compensated for rotation + if (renderer.isSVG) { + width = bBox.width; + height = bBox.height; + + // Workaround for wrong bounding box in IE9 and IE10 (#1101, #1505, #1669, #2568) + if (isMS && styles && styles.fontSize === '11px' && height.toPrecision(3) === '16.9') { + bBox.height = height = 14; + } + + // Adjust for rotated text + if (rotation) { + bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad)); + bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad)); + } + } + + // Cache it + if (cacheKey) { + + // Rotate (#4681) + while (cacheKeys.length > 250) { + delete cache[cacheKeys.shift()]; + } + + if (!cache[cacheKey]) { + cacheKeys.push(cacheKey); + } + cache[cacheKey] = bBox; + } + } + return bBox; + }, + + /** + * Show the element + */ + show: function (inherit) { + return this.attr({ visibility: inherit ? 'inherit' : VISIBLE }); + }, + + /** + * Hide the element + */ + hide: function () { + return this.attr({ visibility: HIDDEN }); + }, + + fadeOut: function (duration) { + var elemWrapper = this; + elemWrapper.animate({ + opacity: 0 + }, { + duration: duration || 150, + complete: function () { + elemWrapper.attr({ y: -9999 }); // #3088, assuming we're only using this for tooltips + } + }); + }, + + /** + * Add the element + * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined + * to append the element to the renderer.box. + */ + add: function (parent) { + + var renderer = this.renderer, + element = this.element, + inserted; + + if (parent) { + this.parentGroup = parent; + } + + // mark as inverted + this.parentInverted = parent && parent.inverted; + + // build formatted text + if (this.textStr !== undefined) { + renderer.buildText(this); + } + + // Mark as added + this.added = true; + + // If we're adding to renderer root, or other elements in the group + // have a z index, we need to handle it + if (!parent || parent.handleZ || this.zIndex) { + inserted = this.zIndexSetter(); + } + + // If zIndex is not handled, append at the end + if (!inserted) { + (parent ? parent.element : renderer.box).appendChild(element); + } + + // fire an event for internal hooks + if (this.onAdd) { + this.onAdd(); + } + + return this; + }, + + /** + * 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) { + var parentNode = element.parentNode; + if (parentNode) { + parentNode.removeChild(element); + } + }, + + /** + * Destroy the element and element wrapper + */ + destroy: function () { + var wrapper = this, + element = wrapper.element || {}, + shadows = wrapper.shadows, + parentToClean = wrapper.renderer.isSVG && element.nodeName === 'SPAN' && wrapper.parentGroup, + grandParent, + key, + i; + + // remove events + element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = element.point = null; + stop(wrapper); // stop running animations + + if (wrapper.clipPath) { + wrapper.clipPath = wrapper.clipPath.destroy(); + } + + // Destroy stops in case this is a gradient object + if (wrapper.stops) { + for (i = 0; i < wrapper.stops.length; i++) { + wrapper.stops[i] = wrapper.stops[i].destroy(); + } + wrapper.stops = null; + } + + // remove element + wrapper.safeRemoveChild(element); + + // destroy shadows + if (shadows) { + each(shadows, function (shadow) { + wrapper.safeRemoveChild(shadow); + }); + } + + // In case of useHTML, clean up empty containers emulating SVG groups (#1960, #2393, #2697). + while (parentToClean && parentToClean.div && parentToClean.div.childNodes.length === 0) { + grandParent = parentToClean.parentGroup; + wrapper.safeRemoveChild(parentToClean.div); + delete parentToClean.div; + parentToClean = grandParent; + } + + // remove from alignObjects + if (wrapper.alignTo) { + erase(wrapper.renderer.alignedObjects, wrapper); + } + + for (key in wrapper) { + delete wrapper[key]; + } + + return null; + }, + + /** + * Add a shadow to the element. Must be done after the element is added to the DOM + * @param {Boolean|Object} shadowOptions + */ + shadow: function (shadowOptions, group, cutOff) { + var shadows = [], + i, + shadow, + element = this.element, + strokeWidth, + shadowWidth, + shadowElementOpacity, + + // compensate for inverted plot area + transform; + + + if (shadowOptions) { + shadowWidth = pick(shadowOptions.width, 3); + shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth; + transform = this.parentInverted ? + '(-1,-1)' : + '(' + pick(shadowOptions.offsetX, 1) + ', ' + pick(shadowOptions.offsetY, 1) + ')'; + for (i = 1; i <= shadowWidth; i++) { + shadow = element.cloneNode(0); + strokeWidth = (shadowWidth * 2) + 1 - (2 * i); + attr(shadow, { + 'isShadow': 'true', + 'stroke': shadowOptions.color || 'black', + 'stroke-opacity': shadowElementOpacity * i, + 'stroke-width': strokeWidth, + 'transform': 'translate' + transform, + 'fill': NONE + }); + if (cutOff) { + attr(shadow, 'height', mathMax(attr(shadow, 'height') - strokeWidth, 0)); + shadow.cutHeight = strokeWidth; + } + + if (group) { + group.element.appendChild(shadow); + } else { + element.parentNode.insertBefore(shadow, element); + } + + shadows.push(shadow); + } + + this.shadows = shadows; + } + return this; + + }, + + xGetter: function (key) { + if (this.element.nodeName === 'circle') { + key = { x: 'cx', y: 'cy' }[key] || key; + } + return this._defaultGetter(key); + }, + + /** + * Get the current value of an attribute or pseudo attribute, used mainly + * for animation. + */ + _defaultGetter: function (key) { + var ret = pick(this[key], this.element ? this.element.getAttribute(key) : null, 0); + + if (/^[\-0-9\.]+$/.test(ret)) { // is numerical + ret = parseFloat(ret); + } + return ret; + }, + + + dSetter: function (value, key, element) { + if (value && value.join) { // join path + value = value.join(' '); + } + if (/(NaN| {2}|^$)/.test(value)) { + value = 'M 0 0'; + } + element.setAttribute(key, value); + + this[key] = value; + }, + dashstyleSetter: function (value) { + var i, + strokeWidth = this['stroke-width']; + + // If "inherit", like maps in IE, assume 1 (#4981). With HC5 and the new strokeWidth + // function, we should be able to use that instead. + if (strokeWidth === 'inherit') { + strokeWidth = 1; + } + value = value && value.toLowerCase(); + if (value) { + value = value + .replace('shortdashdotdot', '3,1,1,1,1,1,') + .replace('shortdashdot', '3,1,1,1') + .replace('shortdot', '1,1,') + .replace('shortdash', '3,1,') + .replace('longdash', '8,3,') + .replace(/dot/g, '1,3,') + .replace('dash', '4,3,') + .replace(/,$/, '') + .split(','); // ending comma + + i = value.length; + while (i--) { + value[i] = pInt(value[i]) * strokeWidth; + } + value = value.join(',') + .replace(/NaN/g, 'none'); // #3226 + this.element.setAttribute('stroke-dasharray', value); + } + }, + alignSetter: function (value) { + this.element.setAttribute('text-anchor', { left: 'start', center: 'middle', right: 'end' }[value]); + }, + opacitySetter: function (value, key, element) { + this[key] = value; + element.setAttribute(key, value); + }, + titleSetter: function (value) { + var titleNode = this.element.getElementsByTagName('title')[0]; + if (!titleNode) { + titleNode = doc.createElementNS(SVG_NS, 'title'); + this.element.appendChild(titleNode); + } + + // Remove text content if it exists + if (titleNode.firstChild) { + titleNode.removeChild(titleNode.firstChild); + } + + titleNode.appendChild( + doc.createTextNode( + (String(pick(value), '')).replace(/<[^>]*>/g, '') // #3276, #3895 + ) + ); + }, + textSetter: function (value) { + if (value !== this.textStr) { + // Delete bBox memo when the text changes + delete this.bBox; + + this.textStr = value; + if (this.added) { + this.renderer.buildText(this); + } + } + }, + fillSetter: function (value, key, element) { + if (typeof value === 'string') { + element.setAttribute(key, value); + } else if (value) { + this.colorGradient(value, key, element); + } + }, + visibilitySetter: function (value, key, element) { + // IE9-11 doesn't handle visibilty:inherit well, so we remove the attribute instead (#2881, #3909) + if (value === 'inherit') { + element.removeAttribute(key); + } else { + element.setAttribute(key, value); + } + }, + zIndexSetter: function (value, key) { + var renderer = this.renderer, + parentGroup = this.parentGroup, + parentWrapper = parentGroup || renderer, + parentNode = parentWrapper.element || renderer.box, + childNodes, + otherElement, + otherZIndex, + element = this.element, + inserted, + run = this.added, + i; + + if (defined(value)) { + element.zIndex = value; // So we can read it for other elements in the group + value = +value; + if (this[key] === value) { // Only update when needed (#3865) + run = false; + } + this[key] = value; + } + + // Insert according to this and other elements' zIndex. Before .add() is called, + // nothing is done. Then on add, or by later calls to zIndexSetter, the node + // is placed on the right place in the DOM. + if (run) { + value = this.zIndex; + + if (value && parentGroup) { + parentGroup.handleZ = true; + } + + childNodes = parentNode.childNodes; + for (i = 0; i < childNodes.length && !inserted; i++) { + otherElement = childNodes[i]; + otherZIndex = otherElement.zIndex; + if (otherElement !== element && ( + // Insert before the first element with a higher zIndex + pInt(otherZIndex) > value || + // If no zIndex given, insert before the first element with a zIndex + (!defined(value) && defined(otherZIndex)) + + )) { + parentNode.insertBefore(element, otherElement); + inserted = true; + } + } + if (!inserted) { + parentNode.appendChild(element); + } + } + return inserted; + }, + _defaultSetter: function (value, key, element) { + element.setAttribute(key, value); + } + }; + + // Some shared setters and getters + SVGElement.prototype.yGetter = SVGElement.prototype.xGetter; + SVGElement.prototype.translateXSetter = SVGElement.prototype.translateYSetter = + SVGElement.prototype.rotationSetter = SVGElement.prototype.verticalAlignSetter = + SVGElement.prototype.scaleXSetter = SVGElement.prototype.scaleYSetter = function (value, key) { + this[key] = value; + this.doTransform = true; + }; + + // WebKit and Batik have problems with a stroke-width of zero, so in this case we remove the + // stroke attribute altogether. #1270, #1369, #3065, #3072. + SVGElement.prototype['stroke-widthSetter'] = SVGElement.prototype.strokeSetter = function (value, key, element) { + this[key] = value; + // Only apply the stroke attribute if the stroke width is defined and larger than 0 + if (this.stroke && this['stroke-width']) { + this.strokeWidth = this['stroke-width']; + SVGElement.prototype.fillSetter.call(this, this.stroke, 'stroke', element); // use prototype as instance may be overridden + element.setAttribute('stroke-width', this['stroke-width']); + this.hasStroke = true; + } else if (key === 'stroke-width' && value === 0 && this.hasStroke) { + element.removeAttribute('stroke'); + this.hasStroke = false; + } + }; + + + /** + * The default SVG renderer + */ + var SVGRenderer = function () { + this.init.apply(this, arguments); + }; + SVGRenderer.prototype = { + Element: SVGElement, + + /** + * Initialize the SVGRenderer + * @param {Object} container + * @param {Number} width + * @param {Number} height + * @param {Boolean} forExport + */ + init: function (container, width, height, style, forExport, allowHTML) { + var renderer = this, + boxWrapper, + element, + desc; + + boxWrapper = renderer.createElement('svg') + .attr({ + version: '1.1' + }) + .css(this.getStyle(style)); + element = boxWrapper.element; + container.appendChild(element); + + // For browsers other than IE, add the namespace attribute (#1978) + if (container.innerHTML.indexOf('xmlns') === -1) { + attr(element, 'xmlns', SVG_NS); + } + + // object properties + renderer.isSVG = true; + renderer.box = element; + renderer.boxWrapper = boxWrapper; + renderer.alignedObjects = []; + + // Page url used for internal references. #24, #672, #1070 + renderer.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ? + win.location.href + .replace(/#.*?$/, '') // remove the hash + .replace(/([\('\)])/g, '\\$1') // escape parantheses and quotes + .replace(/ /g, '%20') : // replace spaces (needed for Safari only) + ''; + + // Add description + desc = this.createElement('desc').add(); + desc.element.appendChild(doc.createTextNode('Created with ' + PRODUCT + ' ' + VERSION)); + + + renderer.defs = this.createElement('defs').add(); + renderer.allowHTML = allowHTML; + renderer.forExport = forExport; + renderer.gradients = {}; // Object where gradient SvgElements are stored + renderer.cache = {}; // Cache for numerical bounding boxes + renderer.cacheKeys = []; + renderer.imgCount = 0; + + renderer.setSize(width, height, false); + + + + // 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) { + renderer.subPixelFix = subPixelFix = function () { + css(container, { left: 0, top: 0 }); + rect = container.getBoundingClientRect(); + css(container, { + left: (mathCeil(rect.left) - rect.left) + PX, + top: (mathCeil(rect.top) - rect.top) + PX + }); + }; + + // run the fix now + subPixelFix(); + + // run it on resize + addEvent(win, 'resize', subPixelFix); + } + }, + + getStyle: function (style) { + this.style = extend({ + fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif', // default font + fontSize: '12px' + }, style); + return this.style; + }, + + /** + * Detect whether the renderer is hidden. This happens when one of the parent elements + * has display: none. #608. + */ + isHidden: function () { + return !this.boxWrapper.getBBox().width; + }, + + /** + * Destroys the renderer and its allocated members. + */ + destroy: function () { + var renderer = this, + rendererDefs = renderer.defs; + renderer.box = null; + renderer.boxWrapper = renderer.boxWrapper.destroy(); + + // Call destroy on all gradient elements + destroyObjectProperties(renderer.gradients || {}); + renderer.gradients = null; + + // Defs are null in VMLRenderer + // Otherwise, destroy them here. + if (rendererDefs) { + renderer.defs = rendererDefs.destroy(); + } + + // Remove sub pixel fix handler + // We need to check that there is a handler, otherwise all functions that are registered for event 'resize' are removed + // See issue #982 + if (renderer.subPixelFix) { + removeEvent(win, 'resize', renderer.subPixelFix); + } + + renderer.alignedObjects = null; + + return null; + }, + + /** + * Create a wrapper for an SVG element + * @param {Object} nodeName + */ + createElement: function (nodeName) { + var wrapper = new this.Element(); + wrapper.init(this, nodeName); + return wrapper; + }, + + /** + * Dummy function for use in canvas renderer + */ + draw: function () {}, + + /** + * Get converted radial gradient attributes + */ + getRadialAttr: function (radialReference, gradAttr) { + return { + cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2], + cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2], + r: gradAttr.r * radialReference[2] + }; + }, + + /** + * Parse a simple HTML string into SVG tspans + * + * @param {Object} textNode The parent text SVG node + */ + buildText: function (wrapper) { + var textNode = wrapper.element, + renderer = this, + forExport = renderer.forExport, + textStr = pick(wrapper.textStr, '').toString(), + hasMarkup = textStr.indexOf('<') !== -1, + lines, + childNodes = textNode.childNodes, + styleRegex, + hrefRegex, + wasTooLong, + parentX = attr(textNode, 'x'), + textStyles = wrapper.styles, + width = wrapper.textWidth, + textLineHeight = textStyles && textStyles.lineHeight, + textShadow = textStyles && textStyles.textShadow, + ellipsis = textStyles && textStyles.textOverflow === 'ellipsis', + i = childNodes.length, + tempParent = width && !wrapper.added && this.box, + getLineHeight = function (tspan) { + return textLineHeight ? + pInt(textLineHeight) : + renderer.fontMetrics( + /(px|em)$/.test(tspan && tspan.style.fontSize) ? + tspan.style.fontSize : + ((textStyles && textStyles.fontSize) || renderer.style.fontSize || 12), + tspan + ).h; + }, + unescapeAngleBrackets = function (inputStr) { + return inputStr.replace(/</g, '<').replace(/>/g, '>'); + }; + + /// remove old text + while (i--) { + textNode.removeChild(childNodes[i]); + } + + // Skip tspans, add text directly to text node. The forceTSpan is a hook + // used in text outline hack. + if (!hasMarkup && !textShadow && !ellipsis && !width && textStr.indexOf(' ') === -1) { + textNode.appendChild(doc.createTextNode(unescapeAngleBrackets(textStr))); + + // Complex strings, add more logic + } else { + + styleRegex = /<.*style="([^"]+)".*>/; + hrefRegex = /<.*href="(http[^"]+)".*>/; + + if (tempParent) { + tempParent.appendChild(textNode); // attach it to the DOM to read offset width + } + + if (hasMarkup) { + lines = textStr + .replace(/<(b|strong)>/g, '') + .replace(/<(i|em)>/g, '') + .replace(/
/g, '
') + .split(//g); + + } else { + lines = [textStr]; + } + + + // Trim empty lines (#5261) + lines = grep(lines, function (line) { + return line !== ''; + }); + + + // build the lines + each(lines, function buildTextLines(line, lineNo) { + var spans, + spanNo = 0; + line = line + .replace(/^\s+|\s+$/g, '') // Trim to prevent useless/costly process on the spaces (#5258) + .replace(//g, '|||'); + spans = line.split('|||'); + + each(spans, function buildTextSpans(span) { + if (span !== '' || spans.length === 1) { + var attributes = {}, + tspan = doc.createElementNS(SVG_NS, 'tspan'), + spanStyle; // #390 + if (styleRegex.test(span)) { + spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2'); + attr(tspan, 'style', spanStyle); + } + if (hrefRegex.test(span) && !forExport) { // Not for export - #1529 + attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"'); + css(tspan, { cursor: 'pointer' }); + } + + span = unescapeAngleBrackets(span.replace(/<(.|\n)*?>/g, '') || ' '); + + // Nested tags aren't supported, and cause crash in Safari (#1596) + if (span !== ' ') { + + // add the text node + tspan.appendChild(doc.createTextNode(span)); + + if (!spanNo) { // first span in a line, align it to the left + if (lineNo && parentX !== null) { + attributes.x = parentX; + } + } else { + attributes.dx = 0; // #16 + } + + // add attributes + attr(tspan, attributes); + + // Append it + textNode.appendChild(tspan); + + // first span on subsequent line, add the line height + if (!spanNo && lineNo) { + + // allow getting the right offset height in exporting in IE + if (!hasSVG && forExport) { + css(tspan, { display: 'block' }); + } + + // Set the line height based on the font size of either + // the text element or the tspan element + attr( + tspan, + 'dy', + getLineHeight(tspan) + ); + } + + /*if (width) { + renderer.breakText(wrapper, width); + }*/ + + // Check width and apply soft breaks or ellipsis + if (width) { + var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273 + hasWhiteSpace = spans.length > 1 || lineNo || (words.length > 1 && textStyles.whiteSpace !== 'nowrap'), + tooLong, + actualWidth, + rest = [], + dy = getLineHeight(tspan), + softLineNo = 1, + rotation = wrapper.rotation, + wordStr = span, // for ellipsis + cursor = wordStr.length, // binary search cursor + bBox; + + while ((hasWhiteSpace || ellipsis) && (words.length || rest.length)) { + wrapper.rotation = 0; // discard rotation when computing box + bBox = wrapper.getBBox(true); + actualWidth = bBox.width; + + // Old IE cannot measure the actualWidth for SVG elements (#2314) + if (!hasSVG && renderer.forExport) { + actualWidth = renderer.measureSpanWidth(tspan.firstChild.data, wrapper.styles); + } + + tooLong = actualWidth > width; + + // For ellipsis, do a binary search for the correct string length + if (wasTooLong === undefined) { + wasTooLong = tooLong; // First time + } + if (ellipsis && wasTooLong) { + cursor /= 2; + + if (wordStr === '' || (!tooLong && cursor < 0.5)) { + words = []; // All ok, break out + } else { + wordStr = span.substring(0, wordStr.length + (tooLong ? -1 : 1) * mathCeil(cursor)); + words = [wordStr + (width > 3 ? '\u2026' : '')]; + tspan.removeChild(tspan.firstChild); + } + + // Looping down, this is the first word sequence that is not too long, + // so we can move on to build the next line. + } else if (!tooLong || words.length === 1) { + words = rest; + rest = []; + + if (words.length) { + softLineNo++; + + tspan = doc.createElementNS(SVG_NS, 'tspan'); + attr(tspan, { + dy: dy, + x: parentX + }); + if (spanStyle) { // #390 + attr(tspan, 'style', spanStyle); + } + 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, '-'))); + } + } + wrapper.rotation = rotation; + } + + spanNo++; + } + } + }); + }); + + if (wasTooLong) { + wrapper.attr('title', wrapper.textStr); + } + if (tempParent) { + tempParent.removeChild(textNode); // attach it to the DOM to read offset width + } + + // Apply the text shadow + if (textShadow && wrapper.applyTextShadow) { + wrapper.applyTextShadow(textShadow); + } + } + }, + + + + /* + breakText: function (wrapper, width) { + var bBox = wrapper.getBBox(), + node = wrapper.element, + textLength = node.textContent.length, + pos = mathRound(width * textLength / bBox.width), // try this position first, based on average character width + increment = 0, + finalPos; + + if (bBox.width > width) { + while (finalPos === undefined) { + textLength = node.getSubStringLength(0, pos); + + if (textLength <= width) { + if (increment === -1) { + finalPos = pos; + } else { + increment = 1; + } + } else { + if (increment === 1) { + finalPos = pos - 1; + } else { + increment = -1; + } + } + pos += increment; + } + } + console.log('width', width, 'stringWidth', node.getSubStringLength(0, finalPos)) + }, + */ + + /** + * Returns white for dark colors and black for bright colors + */ + getContrast: function (color) { + color = Color(color).rgba; + return color[0] + color[1] + color[2] > 384 ? '#000000' : '#FFFFFF'; + }, + + /** + * Create a button with preset states + * @param {String} text + * @param {Number} x + * @param {Number} y + * @param {Function} callback + * @param {Object} normalState + * @param {Object} hoverState + * @param {Object} pressedState + */ + button: function (text, x, y, callback, normalState, hoverState, pressedState, disabledState, shape) { + var label = this.label(text, x, y, shape, null, null, null, null, 'button'), + curState = 0, + stateOptions, + stateStyle, + normalStyle, + hoverStyle, + pressedStyle, + disabledStyle, + verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 }; + + // Normal state - prepare the attributes + normalState = merge({ + 'stroke-width': 1, + stroke: '#CCCCCC', + fill: { + linearGradient: verticalGradient, + stops: [ + [0, '#FEFEFE'], + [1, '#F6F6F6'] + ] + }, + r: 2, + padding: 5, + style: { + color: 'black' + } + }, normalState); + normalStyle = normalState.style; + delete normalState.style; + + // Hover state + hoverState = merge(normalState, { + stroke: '#68A', + fill: { + linearGradient: verticalGradient, + stops: [ + [0, '#FFF'], + [1, '#ACF'] + ] + } + }, hoverState); + hoverStyle = hoverState.style; + delete hoverState.style; + + // Pressed state + pressedState = merge(normalState, { + stroke: '#68A', + fill: { + linearGradient: verticalGradient, + stops: [ + [0, '#9BD'], + [1, '#CDF'] + ] + } + }, pressedState); + pressedStyle = pressedState.style; + delete pressedState.style; + + // Disabled state + disabledState = merge(normalState, { + style: { + color: '#CCC' + } + }, disabledState); + disabledStyle = disabledState.style; + delete disabledState.style; + + // Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667). + addEvent(label.element, isMS ? 'mouseover' : 'mouseenter', function () { + if (curState !== 3) { + label.attr(hoverState) + .css(hoverStyle); + } + }); + addEvent(label.element, isMS ? 'mouseout' : 'mouseleave', function () { + if (curState !== 3) { + stateOptions = [normalState, hoverState, pressedState][curState]; + stateStyle = [normalStyle, hoverStyle, pressedStyle][curState]; + label.attr(stateOptions) + .css(stateStyle); + } + }); + + label.setState = function (state) { + label.state = curState = state; + if (!state) { + label.attr(normalState) + .css(normalStyle); + } else if (state === 2) { + label.attr(pressedState) + .css(pressedStyle); + } else if (state === 3) { + label.attr(disabledState) + .css(disabledStyle); + } + }; + + return label + .on('click', function (e) { + if (curState !== 3) { + callback.call(label, e); + } + }) + .attr(normalState) + .css(extend({ cursor: 'default' }, normalStyle)); + }, + + /** + * 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]) { + // Substract due to #1129. Now bottom and left axis gridlines behave the same. + 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) { + var attr = { + fill: NONE + }; + if (isArray(path)) { + attr.d = path; + } else if (isObject(path)) { // attributes + extend(attr, path); + } + return this.createElement('path').attr(attr); + }, + + /** + * 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 }, + wrapper = this.createElement('circle'); + + // Setting x or y translates to cx and cy + wrapper.xSetter = wrapper.ySetter = function (value, key, element) { + element.setAttribute('c' + key, value); + }; + + return wrapper.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) { + var arc; + + if (isObject(x)) { + y = x.y; + r = x.r; + innerR = x.innerR; + start = x.start; + end = x.end; + x = x.x; + } + + // Arcs are defined as symbols for the ability to set + // attributes in attr and animate + arc = this.symbol('arc', x || 0, y || 0, r || 0, r || 0, { + innerR: innerR || 0, + start: start || 0, + end: end || 0 + }); + arc.r = r; // #959 + return arc; + }, + + /** + * 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) { + + r = isObject(x) ? x.r : r; + + var wrapper = this.createElement('rect'), + attribs = isObject(x) ? x : x === UNDEFINED ? {} : { + x: x, + y: y, + width: mathMax(width, 0), + height: mathMax(height, 0) + }; + + if (strokeWidth !== UNDEFINED) { + wrapper.strokeWidth = strokeWidth; + attribs = wrapper.crisp(attribs); + } + + if (r) { + attribs.r = r; + } + + wrapper.rSetter = function (value, key, element) { + attr(element, { + rx: value, + ry: value + }); + }; + + return wrapper.attr(attribs); + }, + + /** + * 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, width, height, options) { + + var ren = this, + 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), + width, + height, + options + ), + + imageRegex = /^url\((.*?)\)$/, + imageSrc, + imageSize, + centerImage; + + if (path) { + + obj = this.path(path); + // expando properties for use in animate and attr + extend(obj, { + symbolName: symbol, + x: x, + y: y, + width: width, + height: height + }); + if (options) { + extend(obj, options); + } + + + // image symbols + } else if (imageRegex.test(symbol)) { + + // On image load, set the size and position + centerImage = function (img, size) { + if (img.element) { // it may be destroyed in the meantime (#1390) + img.attr({ + width: size[0], + height: size[1] + }); + + if (!img.alignByTranslate) { // #185 + img.translate( + mathRound((width - size[0]) / 2), // #1378 + mathRound((height - size[1]) / 2) + ); + } + } + }; + + imageSrc = symbol.match(imageRegex)[1]; + imageSize = symbolSizes[imageSrc] || (options && options.width && options.height && [options.width, options.height]); + + // Ireate the image synchronously, add attribs async + obj = this.image(imageSrc) + .attr({ + x: x, + y: y + }); + obj.isImg = true; + + 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. Due to a bug in IE < 8, + // the created element must be assigned to a variable in order to load (#292). + createElement('img', { + onload: function () { + + // Special case for SVGs on IE11, the width is not accessible until the image is + // part of the DOM (#2854). + if (this.width === 0) { + css(this, { + position: ABSOLUTE, + top: '-999em' + }); + doc.body.appendChild(this); + } + + // Center the image + centerImage(obj, symbolSizes[imageSrc] = [this.width, this.height]); + + // Clean up after #2854 workaround. + if (this.parentNode) { + this.parentNode.removeChild(this); + } + + // Fire the load event when all external images are loaded + ren.imgCount--; + if (!ren.imgCount && charts[ren.chartIndex].onload) { + charts[ren.chartIndex].onload(); + } + }, + src: imageSrc + }); + this.imgCount++; + } + } + + return obj; + }, + + /** + * An extendable collection of functions for defining symbol paths. + */ + symbols: { + 'circle': function (x, y, w, h) { + var cpw = 0.166 * w; + return [ + M, x + w / 2, y, + 'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h, + 'C', x - cpw, y + h, x - cpw, y, x + w / 2, y, + 'Z' + ]; + }, + + 'square': function (x, y, w, h) { + return [ + M, x, y, + L, x + w, y, + x + w, y + h, + x, y + h, + 'Z' + ]; + }, + + 'triangle': function (x, y, w, h) { + return [ + M, x + w / 2, y, + L, x + w, y + h, + x, y + h, + 'Z' + ]; + }, + + 'triangle-down': function (x, y, w, h) { + return [ + M, x, y, + L, x + w, y, + x + w / 2, y + h, + 'Z' + ]; + }, + 'diamond': function (x, y, w, h) { + return [ + M, x + w / 2, y, + L, x + w, y + h / 2, + x + w / 2, y + h, + x, y + h / 2, + 'Z' + ]; + }, + 'arc': function (x, y, w, h, options) { + var start = options.start, + radius = options.r || w || h, + end = options.end - 0.001, // to prevent cos and sin of start and end from becoming equal on 360 arcs (related: #1561) + innerRadius = options.innerR, + open = options.open, + 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, + open ? M : 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, + + open ? '' : 'Z' // close + ]; + }, + + /** + * Callout shape used for default tooltips, also used for rounded rectangles in VML + */ + callout: function (x, y, w, h, options) { + var arrowLength = 6, + halfDistance = 6, + r = mathMin((options && options.r) || 0, w, h), + safeDistance = r + halfDistance, + anchorX = options && options.anchorX, + anchorY = options && options.anchorY, + path; + + path = [ + 'M', x + r, y, + 'L', x + w - r, y, // top side + 'C', x + w, y, x + w, y, x + w, y + r, // top-right corner + 'L', x + w, y + h - r, // right side + 'C', x + w, y + h, x + w, y + h, x + w - r, y + h, // bottom-right corner + 'L', x + r, y + h, // bottom side + 'C', x, y + h, x, y + h, x, y + h - r, // bottom-left corner + 'L', x, y + r, // left side + 'C', x, y, x, y, x + r, y // top-right corner + ]; + + if (anchorX && anchorX > w && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace right side + path.splice(13, 3, + 'L', x + w, anchorY - halfDistance, + x + w + arrowLength, anchorY, + x + w, anchorY + halfDistance, + x + w, y + h - r + ); + } else if (anchorX && anchorX < 0 && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace left side + path.splice(33, 3, + 'L', x, anchorY + halfDistance, + x - arrowLength, anchorY, + x, anchorY - halfDistance, + x, y + r + ); + } else if (anchorY && anchorY > h && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace bottom + path.splice(23, 3, + 'L', anchorX + halfDistance, y + h, + anchorX, y + h + arrowLength, + anchorX - halfDistance, y + h, + x + r, y + h + ); + } else if (anchorY && anchorY < 0 && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace top + path.splice(3, 3, + 'L', anchorX - halfDistance, y, + anchorX, y - arrowLength, + anchorX + halfDistance, y, + w - r, y + ); + } + return path; + } + }, + + /** + * 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; + wrapper.count = 0; + + return wrapper; + }, + + + + + + /** + * 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 renderer = this, + fakeSVG = useCanVG || (!hasSVG && renderer.forExport), + wrapper, + attr = {}; + + if (useHTML && (renderer.allowHTML || !renderer.forExport)) { + return renderer.html(str, x, y); + } + + attr.x = Math.round(x || 0); // X is always needed for line-wrap logic + if (y) { + attr.y = Math.round(y); + } + if (str || str === 0) { + attr.text = str; + } + + wrapper = renderer.createElement('text') + .attr(attr); + + // Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063) + if (fakeSVG) { + wrapper.css({ + position: ABSOLUTE + }); + } + + if (!useHTML) { + wrapper.xSetter = function (value, key, element) { + var tspans = element.getElementsByTagName('tspan'), + tspan, + parentVal = element.getAttribute(key), + i; + for (i = 0; i < tspans.length; i++) { + tspan = tspans[i]; + // If the x values are equal, the tspan represents a linebreak + if (tspan.getAttribute(key) === parentVal) { + tspan.setAttribute(key, value); + } + } + element.setAttribute(key, value); + }; + } + + return wrapper; + }, + + /** + * Utility to return the baseline offset and total line height from the font size + */ + fontMetrics: function (fontSize, elem) { + var lineHeight, + baseline, + style; + + fontSize = fontSize || this.style.fontSize; + if (!fontSize && elem && win.getComputedStyle) { + elem = elem.element || elem; // SVGElement + style = win.getComputedStyle(elem, ''); + fontSize = style && style.fontSize; // #4309, the style doesn't exist inside a hidden iframe in Firefox + } + fontSize = /px/.test(fontSize) ? pInt(fontSize) : /em/.test(fontSize) ? parseFloat(fontSize) * 12 : 12; + + // Empirical values found by comparing font size and bounding box height. + // Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/ + lineHeight = fontSize < 24 ? fontSize + 3 : mathRound(fontSize * 1.2); + baseline = mathRound(lineHeight * 0.8); + + return { + h: lineHeight, + b: baseline, + f: fontSize + }; + }, + + /** + * Correct X and Y positioning of a label for rotation (#1764) + */ + rotCorr: function (baseline, rotation, alterY) { + var y = baseline; + if (rotation && alterY) { + y = mathMax(y * mathCos(rotation * deg2rad), 4); + } + return { + x: (-baseline / 3) * mathSin(rotation * deg2rad), + y: y + }; + }, + + /** + * Add a label, a text item that can hold a colored or gradient background + * as well as a border and shadow. + * @param {string} str + * @param {Number} x + * @param {Number} y + * @param {String} shape + * @param {Number} anchorX In case the shape has a pointer, like a flag, this is the + * coordinates it should be pinned to + * @param {Number} anchorY + * @param {Boolean} baseline Whether to position the label relative to the text baseline, + * like renderer.text, or to the upper border of the rectangle. + * @param {String} className Class name for the group + */ + label: function (str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) { + + var renderer = this, + wrapper = renderer.g(className), + text = renderer.text('', 0, 0, useHTML) + .attr({ + zIndex: 1 + }), + //.add(wrapper), + box, + bBox, + alignFactor = 0, + padding = 3, + paddingLeft = 0, + width, + height, + wrapperX, + wrapperY, + crispAdjust = 0, + deferredAttr = {}, + baselineOffset, + needsBox, + updateBoxSize, + updateTextPadding, + boxAttr; + + /** + * This function runs after the label is added to the DOM (when the bounding box is + * available), and after the text of the label is updated to detect the new bounding + * box and reflect it in the border box. + */ + updateBoxSize = function () { + var boxX, + boxY, + style = text.element.style; + + bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && defined(text.textStr) && + text.getBBox(); //#3295 && 3514 box failure when string equals 0 + wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft; + wrapper.height = (height || bBox.height || 0) + 2 * padding; + + // update the label-scoped y offset + baselineOffset = padding + renderer.fontMetrics(style && style.fontSize, text).b; + + + if (needsBox) { + + if (!box) { + // create the border box if it is not already present + boxX = crispAdjust; + boxY = (baseline ? -baselineOffset : 0) + crispAdjust; + wrapper.box = box = renderer.symbols[shape] ? // Symbol definition exists (#5324) + renderer.symbol(shape, boxX, boxY, wrapper.width, wrapper.height, deferredAttr) : + renderer.rect(boxX, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]); + + if (!box.isImg) { // #4324, fill "none" causes it to be ignored by mouse events in IE + box.attr('fill', NONE); + } + box.add(wrapper); + } + + // apply the box attributes + if (!box.isImg) { // #1630 + box.attr(extend({ + width: mathRound(wrapper.width), + height: mathRound(wrapper.height) + }, deferredAttr)); + } + deferredAttr = null; + } + }; + + /** + * This function runs after setting text or padding, but only if padding is changed + */ + updateTextPadding = function () { + var styles = wrapper.styles, + textAlign = styles && styles.textAlign, + x = paddingLeft + padding, + y; + + // determin y based on the baseline + y = baseline ? 0 : baselineOffset; + + // compensate for alignment + if (defined(width) && bBox && (textAlign === 'center' || textAlign === 'right')) { + x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width); + } + + // update if anything changed + if (x !== text.x || y !== text.y) { + text.attr('x', x); + if (y !== UNDEFINED) { + text.attr('y', y); + } + } + + // record current values + text.x = x; + text.y = y; + }; + + /** + * Set a box attribute, or defer it if the box is not yet created + * @param {Object} key + * @param {Object} value + */ + boxAttr = function (key, value) { + if (box) { + box.attr(key, value); + } else { + deferredAttr[key] = value; + } + }; + + /** + * After the text element is added, get the desired size of the border box + * and add it before the text in the DOM. + */ + wrapper.onAdd = function () { + text.add(wrapper); + wrapper.attr({ + text: (str || str === 0) ? str : '', // alignment is available now // #3295: 0 not rendered if given as a value + x: x, + y: y + }); + + if (box && defined(anchorX)) { + wrapper.attr({ + anchorX: anchorX, + anchorY: anchorY + }); + } + }; + + /* + * Add specific attribute setters. + */ + + // only change local variables + wrapper.widthSetter = function (value) { + width = value; + }; + wrapper.heightSetter = function (value) { + height = value; + }; + wrapper.paddingSetter = function (value) { + if (defined(value) && value !== padding) { + padding = wrapper.padding = value; + updateTextPadding(); + } + }; + wrapper.paddingLeftSetter = function (value) { + if (defined(value) && value !== paddingLeft) { + paddingLeft = value; + updateTextPadding(); + } + }; + + + // change local variable and prevent setting attribute on the group + wrapper.alignSetter = function (value) { + value = { left: 0, center: 0.5, right: 1 }[value]; + if (value !== alignFactor) { + alignFactor = value; + if (bBox) { // Bounding box exists, means we're dynamically changing + wrapper.attr({ x: wrapperX }); // #5134 + } + } + }; + + // apply these to the box and the text alike + wrapper.textSetter = function (value) { + if (value !== UNDEFINED) { + text.textSetter(value); + } + updateBoxSize(); + updateTextPadding(); + }; + + // apply these to the box but not to the text + wrapper['stroke-widthSetter'] = function (value, key) { + if (value) { + needsBox = true; + } + crispAdjust = value % 2 / 2; + boxAttr(key, value); + }; + wrapper.strokeSetter = wrapper.fillSetter = wrapper.rSetter = function (value, key) { + if (key === 'fill' && value) { + needsBox = true; + } + boxAttr(key, value); + }; + wrapper.anchorXSetter = function (value, key) { + anchorX = value; + boxAttr(key, mathRound(value) - crispAdjust - wrapperX); + }; + wrapper.anchorYSetter = function (value, key) { + anchorY = value; + boxAttr(key, value - wrapperY); + }; + + // rename attributes + wrapper.xSetter = function (value) { + wrapper.x = value; // for animation getter + if (alignFactor) { + value -= alignFactor * ((width || bBox.width) + 2 * padding); + } + wrapperX = mathRound(value); + wrapper.attr('translateX', wrapperX); + }; + wrapper.ySetter = function (value) { + wrapperY = wrapper.y = mathRound(value); + wrapper.attr('translateY', wrapperY); + }; + + // Redirect certain methods to either the box or the text + var baseCss = wrapper.css; + return extend(wrapper, { + /** + * Pick up some properties and apply them to the text instead of the wrapper + */ + css: function (styles) { + if (styles) { + var textStyles = {}; + styles = merge(styles); // create a copy to avoid altering the original object (#537) + each(wrapper.textProps, function (prop) { + if (styles[prop] !== UNDEFINED) { + textStyles[prop] = styles[prop]; + delete styles[prop]; + } + }); + text.css(textStyles); + } + return baseCss.call(wrapper, styles); + }, + /** + * Return the bounding box of the box, not the group + */ + getBBox: function () { + return { + width: bBox.width + 2 * padding, + height: bBox.height + 2 * padding, + x: bBox.x - padding, + y: bBox.y - padding + }; + }, + /** + * Apply the shadow to the box + */ + shadow: function (b) { + if (box) { + box.shadow(b); + } + return wrapper; + }, + /** + * Destroy and release memory. + */ + destroy: function () { + + // Added by button implementation + removeEvent(wrapper.element, 'mouseenter'); + removeEvent(wrapper.element, 'mouseleave'); + + if (text) { + text = text.destroy(); + } + if (box) { + box = box.destroy(); + } + // Call base implementation to destroy the rest + SVGElement.prototype.destroy.call(wrapper); + + // Release local pointers (#1298) + wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = null; + } + }); + } + }; // end SVGRenderer + + + // general renderer + Renderer = SVGRenderer; + // extend SvgElement for useHTML option + extend(SVGElement.prototype, { + /** + * Apply CSS to HTML elements. This is used in text within SVG rendering and + * by the VML renderer + */ + htmlCss: function (styles) { + var wrapper = this, + element = wrapper.element, + textWidth = styles && element.tagName === 'SPAN' && styles.width; + + if (textWidth) { + delete styles.width; + wrapper.textWidth = textWidth; + wrapper.updateTransform(); + } + if (styles && styles.textOverflow === 'ellipsis') { + styles.whiteSpace = 'nowrap'; + styles.overflow = 'hidden'; + } + wrapper.styles = extend(wrapper.styles, styles); + css(wrapper.element, styles); + + return wrapper; + }, + + /** + * VML and useHTML method for calculating the bounding box based on offsets + * @param {Boolean} refresh Whether to force a fresh value from the DOM or to + * use the cached value + * + * @return {Object} A hash containing values for x, y, width and height + */ + + htmlGetBBox: function () { + var wrapper = this, + element = wrapper.element; + + // faking getBBox in exported SVG in legacy IE + // faking getBBox in exported SVG in legacy IE (is this a duplicate of the fix for #1079?) + if (element.nodeName === 'text') { + element.style.position = ABSOLUTE; + } + + return { + x: element.offsetLeft, + y: element.offsetTop, + width: element.offsetWidth, + height: element.offsetHeight + }; + }, + + /** + * VML override private method to update elements based on internal + * properties based on SVG transform + */ + htmlUpdateTransform: function () { + // aligning non added elements is expensive + if (!this.added) { + this.alignOnAdd = true; + return; + } + + var wrapper = this, + renderer = wrapper.renderer, + 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], + shadows = wrapper.shadows, + styles = wrapper.styles; + + // apply translate + css(elem, { + marginLeft: translateX, + marginTop: translateY + }); + if (shadows) { // used in labels/tooltip + each(shadows, function (shadow) { + css(shadow, { + marginLeft: translateX + 1, + marginTop: translateY + 1 + }); + }); + } + + // apply inversion + if (wrapper.inverted) { // wrapper is a group + each(elem.childNodes, function (child) { + renderer.invertChild(child, elem); + }); + } + + if (elem.tagName === 'SPAN') { + + var rotation = wrapper.rotation, + baseline, + textWidth = pInt(wrapper.textWidth), + whiteSpace = styles && styles.whiteSpace, + currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth, wrapper.textAlign].join(','); + + if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed + + + baseline = renderer.fontMetrics(elem.style.fontSize).b; + + // Renderer specific handling of span rotation + if (defined(rotation)) { + wrapper.setSpanRotation(rotation, alignCorrection, baseline); + } + + // Update textWidth + if (elem.offsetWidth > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254 + css(elem, { + width: textWidth + PX, + display: 'block', + whiteSpace: whiteSpace || 'normal' // #3331 + }); + wrapper.hasTextWidth = true; + } else if (wrapper.hasTextWidth) { // #4928 + css(elem, { + width: '', + display: '', + whiteSpace: whiteSpace || 'nowrap' + }); + wrapper.hasTextWidth = false; + } + + wrapper.getSpanCorrection(wrapper.hasTextWidth ? textWidth : elem.offsetWidth, baseline, alignCorrection, rotation, align); + } + + // apply position with correction + css(elem, { + left: (x + (wrapper.xCorr || 0)) + PX, + top: (y + (wrapper.yCorr || 0)) + PX + }); + + // force reflow in webkit to apply the left and top on useHTML element (#1249) + if (isWebKit) { + baseline = elem.offsetHeight; // assigned to baseline for lint purpose + } + + // record current text transform + wrapper.cTT = currentTextTransform; + } + }, + + /** + * Set the rotation of an individual HTML span + */ + setSpanRotation: function (rotation, alignCorrection, baseline) { + var rotationStyle = {}, + cssTransformKey = isMS ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : isOpera ? '-o-transform' : ''; + + rotationStyle[cssTransformKey] = rotationStyle.transform = 'rotate(' + rotation + 'deg)'; + rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = rotationStyle.transformOrigin = (alignCorrection * 100) + '% ' + baseline + 'px'; + css(this.element, rotationStyle); + }, + + /** + * Get the correction in X and Y positioning as the element is rotated. + */ + getSpanCorrection: function (width, baseline, alignCorrection) { + this.xCorr = -width * alignCorrection; + this.yCorr = -baseline; + } + }); + + // Extend SvgRenderer for useHTML option. + extend(SVGRenderer.prototype, { + /** + * Create HTML text node. This is used by the VML renderer as well as the SVG + * renderer through the useHTML option. + * + * @param {String} str + * @param {Number} x + * @param {Number} y + */ + html: function (str, x, y) { + var wrapper = this.createElement('span'), + element = wrapper.element, + renderer = wrapper.renderer, + isSVG = renderer.isSVG, + addSetters = function (element, style) { + // These properties are set as attributes on the SVG group, and as + // identical CSS properties on the div. (#3542) + each(['opacity', 'visibility'], function (prop) { + wrap(element, prop + 'Setter', function (proceed, value, key, elem) { + proceed.call(this, value, key, elem); + style[key] = value; + }); + }); + }; + + // Text setter + wrapper.textSetter = function (value) { + if (value !== element.innerHTML) { + delete this.bBox; + } + element.innerHTML = this.textStr = value; + wrapper.htmlUpdateTransform(); + }; + + // Add setters for the element itself (#4938) + if (isSVG) { // #4938, only for HTML within SVG + addSetters(wrapper, wrapper.element.style); + } + + // Various setters which rely on update transform + wrapper.xSetter = wrapper.ySetter = wrapper.alignSetter = wrapper.rotationSetter = function (value, key) { + if (key === 'align') { + key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML. + } + wrapper[key] = value; + wrapper.htmlUpdateTransform(); + }; + + // Set the default attributes + wrapper + .attr({ + text: str, + x: mathRound(x), + y: mathRound(y) + }) + .css({ + position: ABSOLUTE, + fontFamily: this.style.fontFamily, + fontSize: this.style.fontSize + }); + + // Keep the whiteSpace style outside the wrapper.styles collection + element.style.whiteSpace = 'nowrap'; + + // Use the HTML specific .css method + wrapper.css = wrapper.htmlCss; + + // This is specific for HTML within SVG + if (isSVG) { + wrapper.add = function (svgGroupWrapper) { + + var htmlGroup, + container = renderer.box.parentNode, + parentGroup, + parents = []; + + this.parentGroup = svgGroupWrapper; + + // Create a mock group to hold the HTML elements + if (svgGroupWrapper) { + htmlGroup = svgGroupWrapper.div; + if (!htmlGroup) { + + // Read the parent chain into an array and read from top down + parentGroup = svgGroupWrapper; + while (parentGroup) { + + parents.push(parentGroup); + + // Move up to the next parent group + parentGroup = parentGroup.parentGroup; + } + + // Ensure dynamically updating position when any parent is translated + each(parents.reverse(), function (parentGroup) { + var htmlGroupStyle, + cls = attr(parentGroup.element, 'class'); + + if (cls) { + cls = { className: cls }; + } // else null + + // Create a HTML div and append it to the parent div to emulate + // the SVG group structure + htmlGroup = parentGroup.div = parentGroup.div || createElement(DIV, cls, { + position: ABSOLUTE, + left: (parentGroup.translateX || 0) + PX, + top: (parentGroup.translateY || 0) + PX, + opacity: parentGroup.opacity // #5075 + }, htmlGroup || container); // the top group is appended to container + + // Shortcut + htmlGroupStyle = htmlGroup.style; + + // Set listeners to update the HTML div's position whenever the SVG group + // position is changed + extend(parentGroup, { + translateXSetter: function (value, key) { + htmlGroupStyle.left = value + PX; + parentGroup[key] = value; + parentGroup.doTransform = true; + }, + translateYSetter: function (value, key) { + htmlGroupStyle.top = value + PX; + parentGroup[key] = value; + parentGroup.doTransform = true; + } + }); + addSetters(parentGroup, htmlGroupStyle); + }); + + } + } else { + htmlGroup = container; + } + + htmlGroup.appendChild(element); + + // Shared with VML: + wrapper.added = true; + if (wrapper.alignOnAdd) { + wrapper.htmlUpdateTransform(); + } + + return wrapper; + }; + } + return wrapper; + } + }); + + + /* **************************************************************************** + * * + * 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. * + * * + *****************************************************************************/ + + /** + * @constructor + */ + var VMLRenderer, VMLElement; + if (!hasSVG && !useCanVG) { + + /** + * The VML element wrapper. + */ + VMLElement = { + + /** + * 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 wrapper = this, + markup = ['<', nodeName, ' filled="f" stroked="f"'], + style = ['position: ', ABSOLUTE, ';'], + isDiv = nodeName === DIV; + + // divs and shapes need size + if (nodeName === 'shape' || isDiv) { + style.push('left:0;top:0;width:1px;height:1px;'); + } + style.push('visibility: ', isDiv ? HIDDEN : VISIBLE); + + markup.push(' style="', style.join(''), '"/>'); + + // create element with default attributes and style + if (nodeName) { + markup = isDiv || nodeName === 'span' || nodeName === 'img' ? + markup.join('') : + renderer.prepVML(markup); + wrapper.element = createElement(markup); + } + + wrapper.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 (parent) { + this.parentGroup = parent; + } + + // if the parent group is inverted, apply inversion on all children + if (inverted) { // only on groups + renderer.invertChild(element, parentNode); + } + + // append it + parentNode.appendChild(element); + + // align text after adding to be able to read offset + wrapper.added = true; + if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) { + wrapper.updateTransform(); + } + + // fire an event for internal hooks + if (wrapper.onAdd) { + wrapper.onAdd(); + } + + return wrapper; + }, + + /** + * VML always uses htmlUpdateTransform + */ + updateTransform: SVGElement.prototype.htmlUpdateTransform, + + /** + * Set the rotation of a span with oldIE's filter + */ + setSpanRotation: function () { + // Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented + // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+ + // has support for CSS3 transform. The getBBox method also needs to be updated + // to compensate for the rotation, like it currently does for SVG. + // Test case: http://jsfiddle.net/highcharts/Ybt44/ + + var rotation = this.rotation, + costheta = mathCos(rotation * deg2rad), + sintheta = mathSin(rotation * deg2rad); + + css(this.element, { + filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta, + ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta, + ', sizingMethod=\'auto expand\')'].join('') : NONE + }); + }, + + /** + * Get the positioning correction for the span after rotating. + */ + getSpanCorrection: function (width, baseline, alignCorrection, rotation, align) { + + var costheta = rotation ? mathCos(rotation * deg2rad) : 1, + sintheta = rotation ? mathSin(rotation * deg2rad) : 0, + height = pick(this.elemHeight, this.element.offsetHeight), + quad, + nonLeft = align && align !== 'left'; + + // correct x and y + this.xCorr = costheta < 0 && -width; + this.yCorr = sintheta < 0 && -height; + + // correct for baseline and corners spilling out after rotation + quad = costheta * sintheta < 0; + this.xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection); + this.yCorr -= costheta * baseline * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1); + // correct for the length/height of the text + if (nonLeft) { + this.xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1); + if (rotation) { + this.yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1); + } + css(this.element, { + textAlign: align + }); + } + }, + + /** + * Converts a subset of an SVG path definition to its VML counterpart. Takes an array + * as the parameter and returns a string. + */ + pathToVML: function (value) { + // convert paths + var i = value.length, + path = []; + + 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])) { + path[i] = mathRound(value[i] * 10) - 5; + } else if (value[i] === 'Z') { // close the path + path[i] = 'x'; + } else { + path[i] = value[i]; + + // When the start X and end X coordinates of an arc are too close, + // they are rounded to the same value above. In this case, substract or + // add 1 from the end X and Y positions. #186, #760, #1371, #1410. + if (value.isArc && (value[i] === 'wa' || value[i] === 'at')) { + // Start and end X + if (path[i + 5] === path[i + 7]) { + path[i + 7] += value[i + 7] > value[i + 5] ? 1 : -1; + } + // Start and end Y + if (path[i + 6] === path[i + 8]) { + path[i + 8] += value[i + 8] > value[i + 6] ? 1 : -1; + } + } + } + } + + + // Loop up again to handle path shortcuts (#2132) + /*while (i++ < path.length) { + if (path[i] === 'H') { // horizontal line to + path[i] = 'L'; + path.splice(i + 2, 0, path[i - 1]); + } else if (path[i] === 'V') { // vertical line to + path[i] = 'L'; + path.splice(i + 1, 0, path[i - 2]); + } + }*/ + return path.join(' ') || 'x'; + }, + + /** + * 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, + cssRet; + + if (clipRect) { + clipMembers = clipRect.members; + erase(clipMembers, wrapper); // Ensure unique list of elements (#1258) + clipMembers.push(wrapper); + wrapper.destroyClip = function () { + erase(clipMembers, wrapper); + }; + cssRet = clipRect.getCSS(wrapper); + + } else { + if (wrapper.destroyClip) { + wrapper.destroyClip(); + } + cssRet = { clip: docMode8 ? 'inherit' : 'rect(auto)' }; // #1214 + } + + return wrapper.css(cssRet); + + }, + + /** + * Set styles for the element + * @param {Object} styles + */ + css: SVGElement.prototype.htmlCss, + + /** + * 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. + if (element.parentNode) { + discardElement(element); + } + }, + + /** + * Extend element.destroy by removing it from the clip members array + */ + destroy: function () { + if (this.destroyClip) { + this.destroyClip(); + } + + return SVGElement.prototype.destroy.apply(this); + }, + + /** + * 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; + }, + + /** + * In stacked columns, cut off the shadows so that they don't overlap + */ + cutOffPath: function (path, length) { + + var len; + + path = path.split(/[ ,]/); + len = path.length; + + if (len === 9 || len === 11) { + path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length; + } + return path.join(' '); + }, + + /** + * Apply a drop shadow by copying elements and giving them different strokes + * @param {Boolean|Object} shadowOptions + */ + shadow: function (shadowOptions, group, cutOff) { + var shadows = [], + i, + element = this.element, + renderer = this.renderer, + shadow, + elemStyle = element.style, + markup, + path = element.path, + strokeWidth, + modifiedPath, + shadowWidth, + shadowElementOpacity; + + // some times empty paths are not strings + if (path && typeof path.value !== 'string') { + path = 'x'; + } + modifiedPath = path; + + if (shadowOptions) { + shadowWidth = pick(shadowOptions.width, 3); + shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth; + for (i = 1; i <= 3; i++) { + + strokeWidth = (shadowWidth * 2) + 1 - (2 * i); + + // Cut off shadows for stacked column items + if (cutOff) { + modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5); + } + + markup = ['']; + + shadow = createElement(renderer.prepVML(markup), + null, { + left: pInt(elemStyle.left) + pick(shadowOptions.offsetX, 1), + top: pInt(elemStyle.top) + pick(shadowOptions.offsetY, 1) + } + ); + if (cutOff) { + shadow.cutOff = strokeWidth + 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; + }, + updateShadows: noop, // Used in SVG only + + setAttr: function (key, value) { + if (docMode8) { // IE8 setAttribute bug + this.element[key] = value; + } else { + this.element.setAttribute(key, value); + } + }, + classSetter: function (value) { + // IE8 Standards mode has problems retrieving the className unless set like this + this.element.className = value; + }, + dashstyleSetter: function (value, key, element) { + var strokeElem = element.getElementsByTagName('stroke')[0] || + createElement(this.renderer.prepVML(['']), null, null, element); + strokeElem[key] = value || 'solid'; + this[key] = value; /* because changing stroke-width will change the dash length + and cause an epileptic effect */ + }, + dSetter: function (value, key, element) { + var i, + shadows = this.shadows; + value = value || []; + this.d = value.join && value.join(' '); // used in getter for animation + + element.path = value = this.pathToVML(value); + + // update shadows + if (shadows) { + i = shadows.length; + while (i--) { + shadows[i].path = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value; + } + } + this.setAttr(key, value); + }, + fillSetter: function (value, key, element) { + var nodeName = element.nodeName; + if (nodeName === 'SPAN') { // text color + element.style.color = value; + } else if (nodeName !== 'IMG') { // #1336 + element.filled = value !== NONE; + this.setAttr('fillcolor', this.renderer.color(value, element, key, this)); + } + }, + 'fill-opacitySetter': function (value, key, element) { + createElement( + this.renderer.prepVML(['<', key.split('-')[0], ' opacity="', value, '"/>']), + null, + null, + element + ); + }, + opacitySetter: noop, // Don't bother - animation is too slow and filters introduce artifacts + rotationSetter: function (value, key, element) { + var style = element.style; + this[key] = style[key] = value; // style is for #1873 + + // Correction for the 1x1 size of the shape container. Used in gauge needles. + style.left = -mathRound(mathSin(value * deg2rad) + 1) + PX; + style.top = mathRound(mathCos(value * deg2rad)) + PX; + }, + strokeSetter: function (value, key, element) { + this.setAttr('strokecolor', this.renderer.color(value, element, key, this)); + }, + 'stroke-widthSetter': function (value, key, element) { + element.stroked = !!value; // VML "stroked" attribute + this[key] = value; // used in getter, issue #113 + if (isNumber(value)) { + value += PX; + } + this.setAttr('strokeweight', value); + }, + titleSetter: function (value, key) { + this.setAttr(key, value); + }, + visibilitySetter: function (value, key, element) { + + // Handle inherited visibility + if (value === 'inherit') { + value = VISIBLE; + } + + // Let the shadow follow the main element + if (this.shadows) { + each(this.shadows, function (shadow) { + shadow.style[key] = value; + }); + } + + // Instead of toggling the visibility CSS property, move the div out of the viewport. + // This works around #61 and #586 + if (element.nodeName === 'DIV') { + value = value === HIDDEN ? '-999em' : 0; + + // In order to redraw, IE7 needs the div to be visible when tucked away + // outside the viewport. So the visibility is actually opposite of + // the expected value. This applies to the tooltip only. + if (!docMode8) { + element.style[key] = value ? VISIBLE : HIDDEN; + } + key = 'top'; + } + element.style[key] = value; + }, + xSetter: function (value, key, element) { + this[key] = value; // used in getter + + if (key === 'x') { + key = 'left'; + } else if (key === 'y') { + key = 'top'; + }/* else { + value = mathMax(0, value); // don't set width or height below zero (#311) + }*/ + + // clipping rectangle special + if (this.updateClipping) { + this[key] = value; // the key is now 'left' or 'top' for 'x' and 'y' + this.updateClipping(); + } else { + // normal + element.style[key] = value; + } + }, + zIndexSetter: function (value, key, element) { + element.style[key] = value; + } + }; + VMLElement['stroke-opacitySetter'] = VMLElement['fill-opacitySetter']; + + Highcharts.VMLElement = VMLElement = extendClass(SVGElement, VMLElement); + + // Some shared setters + VMLElement.prototype.ySetter = + VMLElement.prototype.widthSetter = + VMLElement.prototype.heightSetter = + VMLElement.prototype.xSetter; + + + /** + * The VML renderer + */ + var VMLRendererExtension = { // 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, style) { + var renderer = this, + boxWrapper, + box, + css; + + renderer.alignedObjects = []; + + boxWrapper = renderer.createElement(DIV) + .css(extend(this.getStyle(style), { position: 'relative' })); + box = boxWrapper.element; + container.appendChild(boxWrapper.element); + + + // generate the containing box + renderer.isVML = true; + renderer.box = box; + renderer.boxWrapper = boxWrapper; + renderer.gradients = {}; + renderer.cache = {}; // Cache for numerical bounding boxes + renderer.cacheKeys = []; + renderer.imgCount = 0; + + + 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 (#2153, #2368, #2384) + css = 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' + + '{ behavior:url(#default#VML); display: inline-block; } '; + try { + doc.createStyleSheet().cssText = css; + } catch (e) { + doc.styleSheets[0].cssText += css; + } + + } + }, + + + /** + * Detect whether the renderer is hidden. This happens when one of the parent elements + * has display: none + */ + isHidden: function () { + return !this.box.offsetWidth; + }, + + /** + * 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(), + isObj = isObject(x); + + // mimic a rectangle with its style object for automatic updating in attr + return extend(clipRect, { + members: [], + count: 0, + left: (isObj ? x.x : x) + 1, + top: (isObj ? x.y : y) + 1, + width: (isObj ? x.width : width) - 1, + height: (isObj ? x.height : height) - 1, + getCSS: function (wrapper) { + var element = wrapper.element, + nodeName = element.nodeName, + isShape = nodeName === 'shape', + inverted = wrapper.inverted, + rect = this, + top = rect.top - (isShape ? element.offsetTop : 0), + 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 && nodeName === 'DIV') { + 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) { + if (member.element) { // Deleted series, like in stock/members/series-remove demo. Should be removed from members, but this will do. + member.css(clipRect.getCSS(member)); + } + }); + } + }); + + }, + + + /** + * 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, wrapper) { + var renderer = this, + colorObject, + regexRgba = /^rgba/, + markup, + fillType, + ret = NONE; + + // Check for linear or radial gradient + if (color && color.linearGradient) { + fillType = 'gradient'; + } else if (color && color.radialGradient) { + fillType = 'pattern'; + } + + + if (fillType) { + + var stopColor, + stopOpacity, + gradient = color.linearGradient || color.radialGradient, + x1, + y1, + x2, + y2, + opacity1, + opacity2, + color1, + color2, + fillAttr = '', + stops = color.stops, + firstStop, + lastStop, + colors = [], + addFillNode = function () { + // Add the fill subnode. When colors attribute is used, the meanings of opacity and o:opacity2 + // are reversed. + markup = ['']; + createElement(renderer.prepVML(markup), null, null, elem); + }; + + // Extend from 0 to 1 + firstStop = stops[0]; + lastStop = stops[stops.length - 1]; + if (firstStop[0] > 0) { + stops.unshift([ + 0, + firstStop[1] + ]); + } + if (lastStop[0] < 1) { + stops.push([ + 1, + lastStop[1] + ]); + } + + // Compute the stops + each(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; + } + + // Build the color attribute + colors.push((stop[0] * 100) + '% ' + stopColor); + + // Only start and end opacities are allowed, so we use the first and the last + if (!i) { + opacity1 = stopOpacity; + color2 = stopColor; + } else { + opacity2 = stopOpacity; + color1 = stopColor; + } + }); + + // Apply the gradient to fills only. + if (prop === 'fill') { + + // Handle linear gradient angle + if (fillType === 'gradient') { + x1 = gradient.x1 || gradient[0] || 0; + y1 = gradient.y1 || gradient[1] || 0; + x2 = gradient.x2 || gradient[2] || 0; + y2 = gradient.y2 || gradient[3] || 0; + fillAttr = 'angle="' + (90 - math.atan( + (y2 - y1) / // y vector + (x2 - x1) // x vector + ) * 180 / mathPI) + '"'; + + addFillNode(); + + // Radial (circular) gradient + } else { + + var r = gradient.r, + sizex = r * 2, + sizey = r * 2, + cx = gradient.cx, + cy = gradient.cy, + radialReference = elem.radialReference, + bBox, + applyRadialGradient = function () { + if (radialReference) { + bBox = wrapper.getBBox(); + cx += (radialReference[0] - bBox.x) / bBox.width - 0.5; + cy += (radialReference[1] - bBox.y) / bBox.height - 0.5; + sizex *= radialReference[2] / bBox.width; + sizey *= radialReference[2] / bBox.height; + } + fillAttr = 'src="' + defaultOptions.global.VMLRadialGradientURL + '" ' + + 'size="' + sizex + ',' + sizey + '" ' + + 'origin="0.5,0.5" ' + + 'position="' + cx + ',' + cy + '" ' + + 'color2="' + color2 + '" '; + + addFillNode(); + }; + + // Apply radial gradient + if (wrapper.added) { + applyRadialGradient(); + } else { + // We need to know the bounding box to get the size and position right + wrapper.onAdd = applyRadialGradient; + } + + // The fill element's color attribute is broken in IE8 standards mode, so we + // need to set the parent shape's fillcolor attribute instead. + ret = color1; + } + + // Gradients are not supported for VML stroke, return the first color. #722. + } else { + ret = stopColor; + } + + // 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); + + wrapper[prop + '-opacitySetter'](colorObject.get('a'), prop, elem); + + ret = colorObject.get('rgb'); + + + } else { + var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node + if (propNodes.length) { + propNodes[0].opacity = 1; + propNodes[0].type = 'solid'; + } + ret = color; + } + + return ret; + }, + + /** + * 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.attr({ + x: x, + y: y, + width: width, + height: height + }); + } + return obj; + }, + + /** + * For rectangles, VML uses a shape for rect to overcome bugs and rotation problems + */ + createElement: function (nodeName) { + return nodeName === 'rect' ? this.symbol(nodeName) : SVGRenderer.prototype.createElement.call(this, nodeName); + }, + + /** + * In the VML renderer, each child of an inverted div (group) is inverted + * @param {Object} element + * @param {Object} parentNode + */ + invertChild: function (element, parentNode) { + var ren = this, + parentStyle = parentNode.style, + imgStyle = element.tagName === 'IMG' && element.style; // #1111 + + css(element, { + flip: 'x', + left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1), + top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1), + rotation: -90 + }); + + // Recursively invert child elements, needed for nested composite shapes like box plots and error bars. #1680, #1806. + each(element.childNodes, function (child) { + ren.invertChild(child, element); + }); + }, + + /** + * Symbol definitions that override the parent SVG renderer's symbols + * + */ + symbols: { + // VML specific arc function + arc: function (x, y, w, h, options) { + var start = options.start, + end = options.end, + radius = options.r || w || h, + innerRadius = options.innerR, + cosStart = mathCos(start), + sinStart = mathSin(start), + cosEnd = mathCos(end), + sinEnd = mathSin(end), + ret; + + if (end - start === 0) { // no angle, don't show it. + return ['x']; + } + + ret = [ + '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 + ]; + + if (options.open && !innerRadius) { + ret.push( + 'e', + M, + x, // - innerRadius, + y// - innerRadius + ); + } + + ret.push( + '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 + ); + + ret.isArc = true; + return ret; + + }, + // Add circle symbol path. This performs significantly faster than v:oval. + circle: function (x, y, w, h, wrapper) { + + if (wrapper) { + w = h = 2 * wrapper.r; + } + + // Center correction, #1682 + if (wrapper && wrapper.isCircle) { + x -= w / 2; + y -= h / 2; + } + + // Return the path + return [ + 'wa', // clockwisearcto + x, // left + y, // top + x + w, // right + y + h, // bottom + x + w, // start x + y + h / 2, // start y + x + w, // end x + y + h / 2, // 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. When borders are not rounded, + * use the simpler square path, else use the callout path without the arrow. + */ + rect: function (x, y, w, h, options) { + return SVGRenderer.prototype.symbols[ + !defined(options) || !options.r ? 'square' : 'callout' + ].call(0, x, y, w, h, options); + } + } + }; + Highcharts.VMLRenderer = VMLRenderer = function () { + this.init.apply(this, arguments); + }; + VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension); + + // general renderer + Renderer = VMLRenderer; + } + + // This method is used with exporting in old IE, when emulating SVG (see #2314) + SVGRenderer.prototype.measureSpanWidth = function (text, styles) { + var measuringSpan = doc.createElement('span'), + offsetWidth, + textNode = doc.createTextNode(text); + + measuringSpan.appendChild(textNode); + css(measuringSpan, styles); + this.box.appendChild(measuringSpan); + offsetWidth = measuringSpan.offsetWidth; + discardElement(measuringSpan); // #2463 + return offsetWidth; + }; + + + /* **************************************************************************** + * * + * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE * + * * + *****************************************************************************/ + /* **************************************************************************** + * * + * START OF ANDROID < 3 SPECIFIC CODE. THIS CAN BE REMOVED IF YOU'RE NOT * + * TARGETING THAT SYSTEM. * + * * + *****************************************************************************/ + var CanVGRenderer, + CanVGController; + + /** + * Downloads a script and executes a callback when done. + * @param {String} scriptLocation + * @param {Function} callback + */ + function getScript(scriptLocation, callback) { + var head = doc.getElementsByTagName('head')[0], + script = doc.createElement('script'); + + script.type = 'text/javascript'; + script.src = scriptLocation; + script.onload = callback; + + head.appendChild(script); + } + + if (useCanVG) { + /** + * The CanVGRenderer is empty from start to keep the source footprint small. + * When requested, the CanVGController downloads the rest of the source packaged + * together with the canvg library. + */ + Highcharts.CanVGRenderer = CanVGRenderer = function () { + // Override the global SVG namespace to fake SVG/HTML that accepts CSS + SVG_NS = 'http://www.w3.org/1999/xhtml'; + }; + + /** + * Start with an empty symbols object. This is needed when exporting is used (exporting.src.js will add a few symbols), but + * the implementation from SvgRenderer will not be merged in until first render. + */ + CanVGRenderer.prototype.symbols = {}; + + /** + * Handles on demand download of canvg rendering support. + */ + CanVGController = (function () { + // List of renderering calls + var deferredRenderCalls = []; + + /** + * When downloaded, we are ready to draw deferred charts. + */ + function drawDeferred() { + var callLength = deferredRenderCalls.length, + callIndex; + + // Draw all pending render calls + for (callIndex = 0; callIndex < callLength; callIndex++) { + deferredRenderCalls[callIndex](); + } + // Clear the list + deferredRenderCalls = []; + } + + return { + push: function (func, scriptLocation) { + // Only get the script once + if (deferredRenderCalls.length === 0) { + getScript(scriptLocation, drawDeferred); + } + // Register render call + deferredRenderCalls.push(func); + } + }; + }()); + + Renderer = CanVGRenderer; + } // end CanVGRenderer + + /* **************************************************************************** + * * + * END OF ANDROID < 3 SPECIFIC CODE * + * * + *****************************************************************************/ + + /** + * The Tick class + */ + function Tick(axis, pos, type, noLabel) { + this.axis = axis; + this.pos = pos; + this.type = type || ''; + this.isNew = true; + + if (!type && !noLabel) { + this.addLabel(); + } + } + + Tick.prototype = { + /** + * Write the tick label + */ + addLabel: function () { + var tick = this, + axis = tick.axis, + options = axis.options, + chart = axis.chart, + categories = axis.categories, + names = axis.names, + pos = tick.pos, + labelOptions = options.labels, + str, + tickPositions = axis.tickPositions, + isFirst = pos === tickPositions[0], + isLast = pos === tickPositions[tickPositions.length - 1], + value = categories ? + pick(categories[pos], names[pos], pos) : + pos, + label = tick.label, + tickPositionInfo = tickPositions.info, + dateTimeLabelFormat; + + // Set the datetime label format. If a higher rank is set for this position, use that. If not, + // use the general format. + if (axis.isDatetimeAxis && tickPositionInfo) { + dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName]; + } + // set properties for access in render method + tick.isFirst = isFirst; + tick.isLast = isLast; + + // get the string + str = axis.labelFormatter.call({ + axis: axis, + chart: chart, + isFirst: isFirst, + isLast: isLast, + dateTimeLabelFormat: dateTimeLabelFormat, + value: axis.isLog ? correctFloat(axis.lin2log(value)) : value + }); + + // prepare CSS + //css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX }; + + // first call + if (!defined(label)) { + + tick.label = label = + defined(str) && labelOptions.enabled ? + chart.renderer.text( + str, + 0, + 0, + labelOptions.useHTML + ) + //.attr(attr) + // without position absolute, IE export sometimes is wrong + .css(merge(labelOptions.style)) + .add(axis.labelGroup) : + null; + tick.labelLength = label && label.getBBox().width; // Un-rotated length + tick.rotation = 0; // Base value to detect change for new calls to getBBox + + // update + } else if (label) { + label.attr({ text: str }); + } + }, + + /** + * Get the offset height or width of the label + */ + getLabelSize: function () { + return this.label ? + this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] : + 0; + }, + + /** + * Handle the label overflow by adjusting the labels to the left and right edge, or + * hide them if they collide into the neighbour label. + */ + handleOverflow: function (xy) { + var axis = this.axis, + pxPos = xy.x, + chartWidth = axis.chart.chartWidth, + spacing = axis.chart.spacing, + leftBound = pick(axis.labelLeft, mathMin(axis.pos, spacing[3])), + rightBound = pick(axis.labelRight, mathMax(axis.pos + axis.len, chartWidth - spacing[1])), + label = this.label, + rotation = this.rotation, + factor = { left: 0, center: 0.5, right: 1 }[axis.labelAlign], + labelWidth = label.getBBox().width, + slotWidth = axis.getSlotWidth(), + modifiedSlotWidth = slotWidth, + xCorrection = factor, + goRight = 1, + leftPos, + rightPos, + textWidth, + css = {}; + + // Check if the label overshoots the chart spacing box. If it does, move it. + // If it now overshoots the slotWidth, add ellipsis. + if (!rotation) { + leftPos = pxPos - factor * labelWidth; + rightPos = pxPos + (1 - factor) * labelWidth; + + if (leftPos < leftBound) { + modifiedSlotWidth = xy.x + modifiedSlotWidth * (1 - factor) - leftBound; + } else if (rightPos > rightBound) { + modifiedSlotWidth = rightBound - xy.x + modifiedSlotWidth * factor; + goRight = -1; + } + + modifiedSlotWidth = mathMin(slotWidth, modifiedSlotWidth); // #4177 + if (modifiedSlotWidth < slotWidth && axis.labelAlign === 'center') { + xy.x += goRight * (slotWidth - modifiedSlotWidth - xCorrection * (slotWidth - mathMin(labelWidth, modifiedSlotWidth))); + } + // If the label width exceeds the available space, set a text width to be + // picked up below. Also, if a width has been set before, we need to set a new + // one because the reported labelWidth will be limited by the box (#3938). + if (labelWidth > modifiedSlotWidth || (axis.autoRotation && label.styles.width)) { + textWidth = modifiedSlotWidth; + } + + // Add ellipsis to prevent rotated labels to be clipped against the edge of the chart + } else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) { + textWidth = mathRound(pxPos / mathCos(rotation * deg2rad) - leftBound); + } else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) { + textWidth = mathRound((chartWidth - pxPos) / mathCos(rotation * deg2rad)); + } + + if (textWidth) { + css.width = textWidth; + if (!axis.options.labels.style.textOverflow) { + css.textOverflow = 'ellipsis'; + } + label.css(css); + } + }, + + /** + * Get the x and y position for ticks and labels + */ + getPosition: function (horiz, pos, tickmarkOffset, old) { + var axis = this.axis, + chart = axis.chart, + cHeight = (old && chart.oldChartHeight) || chart.chartHeight; + + return { + x: horiz ? + axis.translate(pos + tickmarkOffset, null, null, old) + axis.transB : + axis.left + axis.offset + (axis.opposite ? ((old && chart.oldChartWidth) || chart.chartWidth) - axis.right - axis.left : 0), + + y: horiz ? + cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) : + cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB + }; + + }, + + /** + * Get the x, y position of the tick label + */ + getLabelPosition: function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) { + var axis = this.axis, + transA = axis.transA, + reversed = axis.reversed, + staggerLines = axis.staggerLines, + rotCorr = axis.tickRotCorr || { x: 0, y: 0 }, + yOffset = labelOptions.y, + line; + + if (!defined(yOffset)) { + if (axis.side === 0) { + yOffset = label.rotation ? -8 : -label.getBBox().height; + } else if (axis.side === 2) { + yOffset = rotCorr.y + 8; + } else { + // #3140, #3140 + yOffset = mathCos(label.rotation * deg2rad) * (rotCorr.y - label.getBBox(false, 0).height / 2); + } + } + + x = x + labelOptions.x + rotCorr.x - (tickmarkOffset && horiz ? + tickmarkOffset * transA * (reversed ? -1 : 1) : 0); + y = y + yOffset - (tickmarkOffset && !horiz ? + tickmarkOffset * transA * (reversed ? 1 : -1) : 0); + + // Correct for staggered labels + if (staggerLines) { + line = (index / (step || 1) % staggerLines); + if (axis.opposite) { + line = staggerLines - line - 1; + } + y += line * (axis.labelOffset / staggerLines); + } + + return { + x: x, + y: mathRound(y) + }; + }, + + /** + * Extendible method to return the path of the marker + */ + getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) { + return renderer.crispLine([ + M, + x, + y, + L, + x + (horiz ? 0 : -tickLength), + y + (horiz ? tickLength : 0) + ], tickWidth); + }, + + /** + * Put everything in place + * + * @param index {Number} + * @param old {Boolean} Use old coordinates to prepare an animation into new position + */ + render: function (index, old, opacity) { + var tick = this, + axis = tick.axis, + options = axis.options, + chart = axis.chart, + renderer = chart.renderer, + horiz = axis.horiz, + type = tick.type, + label = tick.label, + pos = tick.pos, + labelOptions = options.labels, + gridLine = tick.gridLine, + gridPrefix = type ? type + 'Grid' : 'grid', + tickPrefix = type ? type + 'Tick' : 'tick', + gridLineWidth = options[gridPrefix + 'LineWidth'], + gridLineColor = options[gridPrefix + 'LineColor'], + dashStyle = options[gridPrefix + 'LineDashStyle'], + tickSize = axis.tickSize(tickPrefix), + tickColor = options[tickPrefix + 'Color'], + gridLinePath, + mark = tick.mark, + markPath, + step = /*axis.labelStep || */labelOptions.step, + attribs, + show = true, + tickmarkOffset = axis.tickmarkOffset, + xy = tick.getPosition(horiz, pos, tickmarkOffset, old), + x = xy.x, + y = xy.y, + reverseCrisp = ((horiz && x === axis.pos + axis.len) || (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687 + + opacity = pick(opacity, 1); + this.isActive = true; + + // create the grid line + if (gridLineWidth) { + gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth * reverseCrisp, old, true); + + if (gridLine === UNDEFINED) { + attribs = { + stroke: gridLineColor, + 'stroke-width': gridLineWidth + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + if (!type) { + attribs.zIndex = 1; + } + if (old) { + attribs.opacity = 0; + } + tick.gridLine = gridLine = + gridLineWidth ? + renderer.path(gridLinePath) + .attr(attribs).add(axis.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[tick.isNew ? 'attr' : 'animate']({ + d: gridLinePath, + opacity: opacity + }); + } + } + + // create the tick mark + if (tickSize) { + if (axis.opposite) { + tickSize[0] = -tickSize[0]; + } + markPath = tick.getMarkPath(x, y, tickSize[0], tickSize[1] * reverseCrisp, horiz, renderer); + if (mark) { // updating + mark.animate({ + d: markPath, + opacity: opacity + }); + } else { // first time + tick.mark = renderer.path( + markPath + ).attr({ + stroke: tickColor, + 'stroke-width': tickSize[1], + opacity: opacity + }).add(axis.axisGroup); + } + } + + // the label is created on init - now move it into place + if (label && isNumber(x)) { + label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step); + + // Apply show first and show last. If the tick is both first and last, it is + // a single centered tick, in which case we show the label anyway (#2100). + if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) || + (tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) { + show = false; + + // Handle label overflow and show or hide accordingly + } else if (horiz && !axis.isRadial && !labelOptions.step && !labelOptions.rotation && !old && opacity !== 0) { + tick.handleOverflow(xy); + } + + // apply step + if (step && index % step) { + // show those indices dividable by step + show = false; + } + + // Set the new position, and show or hide + if (show && isNumber(xy.y)) { + xy.opacity = opacity; + label[tick.isNew ? 'attr' : 'animate'](xy); + tick.isNew = false; + } else { + stop(label); // #5332 + label.attr('y', -9999); // #1338 + } + } + }, + + /** + * Destructor for the tick prototype + */ + destroy: function () { + destroyObjectProperties(this, this.axis); + } + }; + + /** + * The object wrapper for plot lines and plot bands + * @param {Object} options + */ + Highcharts.PlotLineOrBand = function (axis, options) { + this.axis = axis; + + if (options) { + this.options = options; + this.id = options.id; + } + }; + + Highcharts.PlotLineOrBand.prototype = { + + /** + * Render the plot line or plot band. If it is already existing, + * move it. + */ + render: function () { + var plotLine = this, + axis = plotLine.axis, + horiz = axis.horiz, + options = plotLine.options, + optionsLabel = options.label, + label = plotLine.label, + width = options.width, + to = options.to, + from = options.from, + isBand = defined(from) && defined(to), + value = options.value, + dashStyle = options.dashStyle, + svgElem = plotLine.svgElem, + path = [], + addEvent, + eventType, + color = options.color, + zIndex = pick(options.zIndex, 0), + events = options.events, + attribs = {}, + renderer = axis.chart.renderer, + log2lin = axis.log2lin; + + // logarithmic conversion + if (axis.isLog) { + from = log2lin(from); + to = log2lin(to); + value = log2lin(value); + } + + // plot line + if (width) { + path = axis.getPlotLinePath(value, width); + attribs = { + stroke: color, + 'stroke-width': width + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + } else if (isBand) { // plot band + + path = axis.getPlotBandPath(from, to, options); + if (color) { + attribs.fill = color; + } + if (options.borderWidth) { + attribs.stroke = options.borderColor; + attribs['stroke-width'] = options.borderWidth; + } + } else { + return; + } + // zIndex + attribs.zIndex = zIndex; + + // common for lines and bands + if (svgElem) { + if (path) { + svgElem.show(); + svgElem.animate({ d: path }); + } else { + svgElem.hide(); + if (label) { + plotLine.label = label = label.destroy(); + } + } + } 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 && + axis.width > 0 && axis.height > 0 && !path.flat) { + // apply defaults + optionsLabel = merge({ + align: horiz && isBand && 'center', + x: horiz ? !isBand && 4 : 10, + verticalAlign: !horiz && isBand && 'middle', + y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4, + rotation: horiz && !isBand && 90 + }, optionsLabel); + + this.renderLabel(optionsLabel, path, isBand, zIndex); + + } else if (label) { // move out of sight + label.hide(); + } + + // chainable + return plotLine; + }, + + /** + * Render and align label for plot line or band. + */ + renderLabel: function (optionsLabel, path, isBand, zIndex) { + var plotLine = this, + label = plotLine.label, + renderer = plotLine.axis.chart.renderer, + attribs, + xs, + ys, + x, + y; + + // add the SVG element + if (!label) { + attribs = { + align: optionsLabel.textAlign || optionsLabel.align, + rotation: optionsLabel.rotation + }; + + attribs.zIndex = zIndex; + + plotLine.label = label = renderer.text( + optionsLabel.text, + 0, + 0, + optionsLabel.useHTML + ) + .attr(attribs) + .css(optionsLabel.style) + .add(); + } + + // get the bounding box and align the label + // #3000 changed to better handle choice between plotband or plotline + xs = [path[1], path[4], (isBand ? path[6] : path[1])]; + ys = [path[2], path[5], (isBand ? path[7] : path[2])]; + x = arrayMin(xs); + y = arrayMin(ys); + + label.align(optionsLabel, false, { + x: x, + y: y, + width: arrayMax(xs) - x, + height: arrayMax(ys) - y + }); + label.show(); + }, + + /** + * Remove the plot line or band + */ + destroy: function () { + // remove it from the lookup + erase(this.axis.plotLinesAndBands, this); + + delete this.axis; + destroyObjectProperties(this); + } + }; + + /** + * Object with members for extending the Axis prototype + */ + + AxisPlotLineOrBandExtension = { + + /** + * Create the path for a plot band + */ + getPlotBandPath: function (from, to) { + var toPath = this.getPlotLinePath(to, null, null, true), + path = this.getPlotLinePath(from, null, null, true); + + if (path && toPath) { + + // Flat paths don't need labels (#3836) + path.flat = path.toString() === toPath.toString(); + + path.push( + toPath[4], + toPath[5], + toPath[1], + toPath[2] + ); + } else { // outside the axis area + path = null; + } + + return path; + }, + + addPlotBand: function (options) { + return this.addPlotBandOrLine(options, 'plotBands'); + }, + + addPlotLine: function (options) { + return this.addPlotBandOrLine(options, 'plotLines'); + }, + + /** + * Add a plot band or plot line after render time + * + * @param options {Object} The plotBand or plotLine configuration object + */ + addPlotBandOrLine: function (options, coll) { + var obj = new Highcharts.PlotLineOrBand(this, options).render(), + userOptions = this.userOptions; + + if (obj) { // #2189 + // Add it to the user options for exporting and Axis.update + if (coll) { + userOptions[coll] = userOptions[coll] || []; + userOptions[coll].push(options); + } + this.plotLinesAndBands.push(obj); + } + + return obj; + }, + + /** + * Remove a plot band or plot line from the chart by id + * @param {Object} id + */ + removePlotBandOrLine: function (id) { + var plotLinesAndBands = this.plotLinesAndBands, + options = this.options, + userOptions = this.userOptions, + i = plotLinesAndBands.length; + while (i--) { + if (plotLinesAndBands[i].id === id) { + plotLinesAndBands[i].destroy(); + } + } + each([options.plotLines || [], userOptions.plotLines || [], options.plotBands || [], userOptions.plotBands || []], function (arr) { + i = arr.length; + while (i--) { + if (arr[i].id === id) { + erase(arr, arr[i]); + } + } + }); + } + }; + + /** + * Create a new axis object + * @param {Object} chart + * @param {Object} options + */ + var Axis = Highcharts.Axis = function () { + this.init.apply(this, arguments); + }; + + Axis.prototype = { + + /** + * Default options for the X axis - the Y axis has extended defaults + */ + defaultOptions: { + // allowDecimals: null, + // alternateGridColor: null, + // categories: [], + dateTimeLabelFormats: { + millisecond: '%H:%M:%S.%L', + second: '%H:%M:%S', + minute: '%H:%M', + hour: '%H:%M', + day: '%e. %b', + week: '%e. %b', + month: '%b \'%y', + year: '%Y' + }, + endOnTick: false, + gridLineColor: '#D8D8D8', + // gridLineDashStyle: 'solid', + // gridLineWidth: 0, + // reversed: false, + + labels: { + enabled: true, + // rotation: 0, + // align: 'center', + // step: null, + style: { + color: '#606060', + cursor: 'default', + fontSize: '11px' + }, + x: 0 + //y: undefined + /*formatter: function () { + return this.value; + },*/ + }, + lineColor: '#C0D0E0', + lineWidth: 1, + //linkedTo: null, + //max: undefined, + //min: undefined, + minPadding: 0.01, + maxPadding: 0.01, + //minRange: null, + minorGridLineColor: '#E0E0E0', + // minorGridLineDashStyle: null, + minorGridLineWidth: 1, + minorTickColor: '#A0A0A0', + //minorTickInterval: null, + minorTickLength: 2, + minorTickPosition: 'outside', // inside or outside + //minorTickWidth: 0, + //opposite: false, + //offset: 0, + //plotBands: [{ + // events: {}, + // zIndex: 1, + // labels: { align, x, verticalAlign, y, style, rotation, textAlign } + //}], + //plotLines: [{ + // events: {} + // dashStyle: {} + // zIndex: + // labels: { align, x, verticalAlign, y, style, rotation, textAlign } + //}], + //reversed: false, + // showFirstLabel: true, + // showLastLabel: true, + startOfWeek: 1, + startOnTick: false, + tickColor: '#C0D0E0', + //tickInterval: null, + tickLength: 10, + tickmarkPlacement: 'between', // on or between + tickPixelInterval: 100, + tickPosition: 'outside', + //tickWidth: 1, + title: { + //text: null, + align: 'middle', // low, middle or high + //margin: 0 for horizontal, 10 for vertical axes, + //rotation: 0, + //side: 'outside', + style: { + color: '#707070' + } + //x: 0, + //y: 0 + }, + type: 'linear' // linear, logarithmic or datetime + //visible: true + }, + + /** + * This options set extends the defaultOptions for Y axes + */ + defaultYAxisOptions: { + endOnTick: true, + gridLineWidth: 1, + tickPixelInterval: 72, + showLastLabel: true, + labels: { + x: -8 + }, + lineWidth: 0, + maxPadding: 0.05, + minPadding: 0.05, + startOnTick: true, + //tickWidth: 0, + title: { + rotation: 270, + text: 'Values' + }, + stackLabels: { + enabled: false, + //align: dynamic, + //y: dynamic, + //x: dynamic, + //verticalAlign: dynamic, + //textAlign: dynamic, + //rotation: 0, + formatter: function () { + return Highcharts.numberFormat(this.total, -1); + }, + style: merge(defaultPlotOptions.line.dataLabels.style, { color: '#000000' }) + } + }, + + /** + * These options extend the defaultOptions for left axes + */ + defaultLeftAxisOptions: { + labels: { + x: -15 + }, + title: { + rotation: 270 + } + }, + + /** + * These options extend the defaultOptions for right axes + */ + defaultRightAxisOptions: { + labels: { + x: 15 + }, + title: { + rotation: 90 + } + }, + + /** + * These options extend the defaultOptions for bottom axes + */ + defaultBottomAxisOptions: { + labels: { + autoRotation: [-45], + x: 0 + // overflow: undefined, + // staggerLines: null + }, + title: { + rotation: 0 + } + }, + /** + * These options extend the defaultOptions for top axes + */ + defaultTopAxisOptions: { + labels: { + autoRotation: [-45], + x: 0 + // overflow: undefined + // staggerLines: null + }, + title: { + rotation: 0 + } + }, + + /** + * Initialize the axis + */ + init: function (chart, userOptions) { + + + var isXAxis = userOptions.isX, + axis = this; + + axis.chart = chart; + + // Flag, is the axis horizontal + axis.horiz = chart.inverted ? !isXAxis : isXAxis; + + // Flag, isXAxis + axis.isXAxis = isXAxis; + axis.coll = isXAxis ? 'xAxis' : 'yAxis'; + + axis.opposite = userOptions.opposite; // needed in setOptions + axis.side = userOptions.side || (axis.horiz ? + (axis.opposite ? 0 : 2) : // top : bottom + (axis.opposite ? 1 : 3)); // right : left + + axis.setOptions(userOptions); + + + var options = this.options, + type = options.type, + isDatetimeAxis = type === 'datetime'; + + axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format + + + // Flag, stagger lines or not + axis.userOptions = userOptions; + + //axis.axisTitleMargin = UNDEFINED,// = options.title.margin, + axis.minPixelPadding = 0; + + axis.reversed = options.reversed; + axis.visible = options.visible !== false; + axis.zoomEnabled = options.zoomEnabled !== false; + + // Initial categories + axis.categories = options.categories || type === 'category'; + axis.names = axis.names || []; // Preserve on update (#3830) + + // Elements + //axis.axisGroup = UNDEFINED; + //axis.gridGroup = UNDEFINED; + //axis.axisTitle = UNDEFINED; + //axis.axisLine = UNDEFINED; + + // Shorthand types + axis.isLog = type === 'logarithmic'; + axis.isDatetimeAxis = isDatetimeAxis; + + // Flag, if axis is linked to another axis + axis.isLinked = defined(options.linkedTo); + // Linked axis. + //axis.linkedParent = UNDEFINED; + + // Tick positions + //axis.tickPositions = UNDEFINED; // array containing predefined positions + // Tick intervals + //axis.tickInterval = UNDEFINED; + //axis.minorTickInterval = UNDEFINED; + + + // Major ticks + axis.ticks = {}; + axis.labelEdge = []; + // Minor ticks + axis.minorTicks = {}; + + // List of plotLines/Bands + axis.plotLinesAndBands = []; + + // Alternate bands + axis.alternateBands = {}; + + // Axis metrics + //axis.left = UNDEFINED; + //axis.top = UNDEFINED; + //axis.width = UNDEFINED; + //axis.height = UNDEFINED; + //axis.bottom = UNDEFINED; + //axis.right = UNDEFINED; + //axis.transA = UNDEFINED; + //axis.transB = UNDEFINED; + //axis.oldTransA = UNDEFINED; + axis.len = 0; + //axis.oldMin = UNDEFINED; + //axis.oldMax = UNDEFINED; + //axis.oldUserMin = UNDEFINED; + //axis.oldUserMax = UNDEFINED; + //axis.oldAxisLength = UNDEFINED; + axis.minRange = axis.userMinRange = options.minRange || options.maxZoom; + axis.range = options.range; + axis.offset = options.offset || 0; + + + // Dictionary for stacks + axis.stacks = {}; + axis.oldStacks = {}; + axis.stacksTouched = 0; + + // Min and max in the data + //axis.dataMin = UNDEFINED, + //axis.dataMax = UNDEFINED, + + // The axis range + axis.max = null; + axis.min = null; + + // User set min and max + //axis.userMin = UNDEFINED, + //axis.userMax = UNDEFINED, + + // Crosshair options + axis.crosshair = pick(options.crosshair, splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1], false); + // Run Axis + + var eventType, + events = axis.options.events; + + // Register + if (inArray(axis, chart.axes) === -1) { // don't add it again on Axis.update() + if (isXAxis && !this.isColorAxis) { // #2713 + chart.axes.splice(chart.xAxis.length, 0, axis); + } else { + chart.axes.push(axis); + } + + chart[axis.coll].push(axis); + } + + axis.series = axis.series || []; // populated by Series + + // inverted charts have reversed xAxes as default + if (chart.inverted && isXAxis && axis.reversed === UNDEFINED) { + axis.reversed = true; + } + + axis.removePlotBand = axis.removePlotBandOrLine; + axis.removePlotLine = axis.removePlotBandOrLine; + + + // register event listeners + for (eventType in events) { + addEvent(axis, eventType, events[eventType]); + } + + // extend logarithmic axis + if (axis.isLog) { + axis.val2lin = axis.log2lin; + axis.lin2val = axis.lin2log; + } + }, + + /** + * Merge and set options + */ + setOptions: function (userOptions) { + this.options = merge( + this.defaultOptions, + this.isXAxis ? {} : this.defaultYAxisOptions, + [this.defaultTopAxisOptions, this.defaultRightAxisOptions, + this.defaultBottomAxisOptions, this.defaultLeftAxisOptions][this.side], + merge( + defaultOptions[this.coll], // if set in setOptions (#1053) + userOptions + ) + ); + }, + + /** + * The default label formatter. The context is a special config object for the label. + */ + defaultLabelFormatter: function () { + var axis = this.axis, + value = this.value, + categories = axis.categories, + dateTimeLabelFormat = this.dateTimeLabelFormat, + numericSymbols = defaultOptions.lang.numericSymbols, + i = numericSymbols && numericSymbols.length, + multi, + ret, + formatOption = axis.options.labels.format, + + // make sure the same symbol is added for all labels on a linear axis + numericSymbolDetector = axis.isLog ? value : axis.tickInterval; + + if (formatOption) { + ret = format(formatOption, this); + + } else if (categories) { + ret = value; + + } else if (dateTimeLabelFormat) { // datetime axis + ret = dateFormat(dateTimeLabelFormat, value); + + } else if (i && numericSymbolDetector >= 1000) { + // Decide whether we should add a numeric symbol like k (thousands) or M (millions). + // If we are to enable this in tooltip or other places as well, we can move this + // logic to the numberFormatter and enable it by a parameter. + while (i-- && ret === UNDEFINED) { + multi = Math.pow(1000, i + 1); + if (numericSymbolDetector >= multi && (value * 10) % multi === 0 && numericSymbols[i] !== null) { + ret = Highcharts.numberFormat(value / multi, -1) + numericSymbols[i]; + } + } + } + + if (ret === UNDEFINED) { + if (mathAbs(value) >= 10000) { // add thousands separators + ret = Highcharts.numberFormat(value, -1); + + } else { // small numbers + ret = Highcharts.numberFormat(value, -1, UNDEFINED, ''); // #2466 + } + } + + return ret; + }, + + /** + * Get the minimum and maximum for the series of each axis + */ + getSeriesExtremes: function () { + var axis = this, + chart = axis.chart; + + axis.hasVisibleSeries = false; + + // Reset properties in case we're redrawing (#3353) + axis.dataMin = axis.dataMax = axis.threshold = null; + axis.softThreshold = !axis.isXAxis; + + if (axis.buildStacks) { + axis.buildStacks(); + } + + // loop through this axis' series + each(axis.series, function (series) { + + if (series.visible || !chart.options.chart.ignoreHiddenSeries) { + + var seriesOptions = series.options, + xData, + threshold = seriesOptions.threshold, + seriesDataMin, + seriesDataMax; + + axis.hasVisibleSeries = true; + + // Validate threshold in logarithmic axes + if (axis.isLog && threshold <= 0) { + threshold = null; + } + + // Get dataMin and dataMax for X axes + if (axis.isXAxis) { + xData = series.xData; + if (xData.length) { + // If xData contains values which is not numbers, then filter them out. + // To prevent performance hit, we only do this after we have already + // found seriesDataMin because in most cases all data is valid. #5234. + seriesDataMin = arrayMin(xData); + if (!isNumber(seriesDataMin) && !(seriesDataMin instanceof Date)) { // Date for #5010 + xData = grep(xData, function (x) { + return isNumber(x); + }); + seriesDataMin = arrayMin(xData); // Do it again with valid data + } + + axis.dataMin = mathMin(pick(axis.dataMin, xData[0]), seriesDataMin); + axis.dataMax = mathMax(pick(axis.dataMax, xData[0]), arrayMax(xData)); + + } + + // Get dataMin and dataMax for Y axes, as well as handle stacking and processed data + } else { + + // Get this particular series extremes + series.getExtremes(); + seriesDataMax = series.dataMax; + seriesDataMin = series.dataMin; + + // Get the dataMin and dataMax so far. If percentage is used, the min and max are + // always 0 and 100. If seriesDataMin and seriesDataMax is null, then series + // doesn't have active y data, we continue with nulls + if (defined(seriesDataMin) && defined(seriesDataMax)) { + axis.dataMin = mathMin(pick(axis.dataMin, seriesDataMin), seriesDataMin); + axis.dataMax = mathMax(pick(axis.dataMax, seriesDataMax), seriesDataMax); + } + + // Adjust to threshold + if (defined(threshold)) { + axis.threshold = threshold; + } + // If any series has a hard threshold, it takes precedence + if (!seriesOptions.softThreshold || axis.isLog) { + axis.softThreshold = false; + } + } + } + }); + }, + + /** + * Translate from axis value to pixel position on the chart, or back + * + */ + translate: function (val, backwards, cvsCoord, old, handleLog, pointPlacement) { + var axis = this.linkedParent || this, // #1417 + sign = 1, + cvsOffset = 0, + localA = old ? axis.oldTransA : axis.transA, + localMin = old ? axis.oldMin : axis.min, + returnValue, + minPixelPadding = axis.minPixelPadding, + doPostTranslate = (axis.isOrdinal || axis.isBroken || (axis.isLog && handleLog)) && axis.lin2val; + + if (!localA) { + localA = axis.transA; + } + + // In vertical axes, the canvas coordinates start from 0 at the top like in + // SVG. + if (cvsCoord) { + sign *= -1; // canvas coordinates inverts the value + cvsOffset = axis.len; + } + + // Handle reversed axis + if (axis.reversed) { + sign *= -1; + cvsOffset -= sign * (axis.sector || axis.len); + } + + // From pixels to value + if (backwards) { // reverse translation + + val = val * sign + cvsOffset; + val -= minPixelPadding; + returnValue = val / localA + localMin; // from chart pixel to value + if (doPostTranslate) { // log and ordinal axes + returnValue = axis.lin2val(returnValue); + } + + // From value to pixels + } else { + if (doPostTranslate) { // log and ordinal axes + val = axis.val2lin(val); + } + if (pointPlacement === 'between') { + pointPlacement = 0.5; + } + returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding) + + (isNumber(pointPlacement) ? localA * pointPlacement * axis.pointRange : 0); + } + + return returnValue; + }, + + /** + * Utility method to translate an axis value to pixel position. + * @param {Number} value A value in terms of axis units + * @param {Boolean} paneCoordinates Whether to return the pixel coordinate relative to the chart + * or just the axis/pane itself. + */ + toPixels: function (value, paneCoordinates) { + return this.translate(value, false, !this.horiz, null, true) + (paneCoordinates ? 0 : this.pos); + }, + + /* + * Utility method to translate a pixel position in to an axis value + * @param {Number} pixel The pixel value coordinate + * @param {Boolean} paneCoordiantes Whether the input pixel is relative to the chart or just the + * axis/pane itself. + */ + toValue: function (pixel, paneCoordinates) { + return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, null, true); + }, + + /** + * 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, force, translatedValue) { + var axis = this, + chart = axis.chart, + axisLeft = axis.left, + axisTop = axis.top, + x1, + y1, + x2, + y2, + cHeight = (old && chart.oldChartHeight) || chart.chartHeight, + cWidth = (old && chart.oldChartWidth) || chart.chartWidth, + skip, + transB = axis.transB, + /** + * Check if x is between a and b. If not, either move to a/b or skip, + * depending on the force parameter. + */ + between = function (x, a, b) { + if (x < a || x > b) { + if (force) { + x = mathMin(mathMax(a, x), b); + } else { + skip = true; + } + } + return x; + }; + + translatedValue = pick(translatedValue, axis.translate(value, null, null, old)); + x1 = x2 = mathRound(translatedValue + transB); + y1 = y2 = mathRound(cHeight - translatedValue - transB); + if (!isNumber(translatedValue)) { // no min or max + skip = true; + + } else if (axis.horiz) { + y1 = axisTop; + y2 = cHeight - axis.bottom; + x1 = x2 = between(x1, axisLeft, axisLeft + axis.width); + } else { + x1 = axisLeft; + x2 = cWidth - axis.right; + y1 = y2 = between(y1, axisTop, axisTop + axis.height); + } + return skip && !force ? + null : + chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 1); + }, + + /** + * Set the tick positions of a linear axis to round values like whole tens or every five. + */ + getLinearTickPositions: function (tickInterval, min, max) { + var pos, + lastPos, + roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval), + roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval), + tickPositions = []; + + // For single points, add a tick regardless of the relative position (#2662) + if (min === max && isNumber(min)) { + return [min]; + } + + // Populate the intermediate values + pos = roundedMin; + while (pos <= roundedMax) { + + // Place the tick on the rounded value + tickPositions.push(pos); + + // Always add the raw tickInterval, not the corrected one. + pos = correctFloat(pos + tickInterval); + + // If the interval is not big enough in the current min - max range to actually increase + // the loop variable, we need to break out to prevent endless loop. Issue #619 + if (pos === lastPos) { + break; + } + + // Record the last value + lastPos = pos; + } + return tickPositions; + }, + + /** + * Return the minor tick positions. For logarithmic axes, reuse the same logic + * as for major ticks. + */ + getMinorTickPositions: function () { + var axis = this, + options = axis.options, + tickPositions = axis.tickPositions, + minorTickInterval = axis.minorTickInterval, + minorTickPositions = [], + pos, + i, + pointRangePadding = axis.pointRangePadding || 0, + min = axis.min - pointRangePadding, // #1498 + max = axis.max + pointRangePadding, // #1498 + range = max - min, + len; + + // If minor ticks get too dense, they are hard to read, and may cause long running script. So we don't draw them. + if (range && range / minorTickInterval < axis.len / 3) { // #3875 + + if (axis.isLog) { + len = tickPositions.length; + for (i = 1; i < len; i++) { + minorTickPositions = minorTickPositions.concat( + axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true) + ); + } + } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314 + minorTickPositions = minorTickPositions.concat( + axis.getTimeTicks( + axis.normalizeTimeTickInterval(minorTickInterval), + min, + max, + options.startOfWeek + ) + ); + } else { + for (pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval) { + minorTickPositions.push(pos); + } + } + } + + if (minorTickPositions.length !== 0) { // don't change the extremes, when there is no minor ticks + axis.trimTicks(minorTickPositions, options.startOnTick, options.endOnTick); // #3652 #3743 #1498 + } + return minorTickPositions; + }, + + /** + * Adjust the min and max for the minimum range. Keep in mind that the series data is + * not yet processed, so we don't have information on data cropping and grouping, or + * updated axis.pointRange or series.pointRange. The data can't be processed until + * we have finally established min and max. + */ + adjustForMinRange: function () { + var axis = this, + options = axis.options, + min = axis.min, + max = axis.max, + zoomOffset, + spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange, + closestDataRange, + i, + distance, + xData, + loopLength, + minArgs, + maxArgs, + minRange; + + // Set the automatic minimum range based on the closest point distance + if (axis.isXAxis && axis.minRange === UNDEFINED && !axis.isLog) { + + if (defined(options.min) || defined(options.max)) { + axis.minRange = null; // don't do this again + + } else { + + // Find the closest distance between raw data points, as opposed to + // closestPointRange that applies to processed points (cropped and grouped) + each(axis.series, function (series) { + xData = series.xData; + loopLength = series.xIncrement ? 1 : xData.length - 1; + for (i = loopLength; i > 0; i--) { + distance = xData[i] - xData[i - 1]; + if (closestDataRange === UNDEFINED || distance < closestDataRange) { + closestDataRange = distance; + } + } + }); + axis.minRange = mathMin(closestDataRange * 5, axis.dataMax - axis.dataMin); + } + } + + // if minRange is exceeded, adjust + if (max - min < axis.minRange) { + minRange = axis.minRange; + zoomOffset = (minRange - max + min) / 2; + + // if min and max options have been set, don't go beyond it + minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)]; + if (spaceAvailable) { // if space is available, stay within the data range + minArgs[2] = axis.dataMin; + } + min = arrayMax(minArgs); + + maxArgs = [min + minRange, pick(options.max, min + minRange)]; + if (spaceAvailable) { // if space is availabe, stay within the data range + maxArgs[2] = axis.dataMax; + } + + max = arrayMin(maxArgs); + + // now if the max is adjusted, adjust the min back + if (max - min < minRange) { + minArgs[0] = max - minRange; + minArgs[1] = pick(options.min, max - minRange); + min = arrayMax(minArgs); + } + } + + // Record modified extremes + axis.min = min; + axis.max = max; + }, + + /** + * Find the closestPointRange across all series + */ + getClosest: function () { + var ret; + each(this.series, function (series) { + var seriesClosest = series.closestPointRange; + if (!series.noSharedTooltip && defined(seriesClosest)) { + ret = defined(ret) ? + mathMin(ret, seriesClosest) : + seriesClosest; + } + }); + return ret; + }, + + /** + * Update translation information + */ + setAxisTranslation: function (saveOld) { + var axis = this, + range = axis.max - axis.min, + pointRange = axis.axisPointRange || 0, + closestPointRange, + minPointOffset = 0, + pointRangePadding = 0, + linkedParent = axis.linkedParent, + ordinalCorrection, + hasCategories = !!axis.categories, + transA = axis.transA, + isXAxis = axis.isXAxis; + + // Adjust translation for padding. Y axis with categories need to go through the same (#1784). + if (isXAxis || hasCategories || pointRange) { + if (linkedParent) { + minPointOffset = linkedParent.minPointOffset; + pointRangePadding = linkedParent.pointRangePadding; + + } else { + + // Get the closest points + closestPointRange = axis.getClosest(); + + each(axis.series, function (series) { + var seriesPointRange = hasCategories ? + 1 : + (isXAxis ? + pick(series.options.pointRange, closestPointRange, 0) : + (axis.axisPointRange || 0)), // #2806 + pointPlacement = series.options.pointPlacement; + + pointRange = mathMax(pointRange, seriesPointRange); + + if (!axis.single) { + // minPointOffset is the value padding to the left of the axis in order to make + // room for points with a pointRange, typically columns. When the pointPlacement option + // is 'between' or 'on', this padding does not apply. + minPointOffset = mathMax( + minPointOffset, + isString(pointPlacement) ? 0 : seriesPointRange / 2 + ); + + // Determine the total padding needed to the length of the axis to make room for the + // pointRange. If the series' pointPlacement is 'on', no padding is added. + pointRangePadding = mathMax( + pointRangePadding, + pointPlacement === 'on' ? 0 : seriesPointRange + ); + } + }); + } + + // Record minPointOffset and pointRangePadding + ordinalCorrection = axis.ordinalSlope && closestPointRange ? axis.ordinalSlope / closestPointRange : 1; // #988, #1853 + axis.minPointOffset = minPointOffset = minPointOffset * ordinalCorrection; + axis.pointRangePadding = pointRangePadding = pointRangePadding * ordinalCorrection; + + // pointRange means the width reserved for each point, like in a column chart + axis.pointRange = mathMin(pointRange, range); + + // closestPointRange means the closest distance between points. In columns + // it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange + // is some other value + if (isXAxis) { + axis.closestPointRange = closestPointRange; + } + } + + // Secondary values + if (saveOld) { + axis.oldTransA = transA; + } + axis.translationSlope = axis.transA = transA = axis.len / ((range + pointRangePadding) || 1); + axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend + axis.minPixelPadding = transA * minPointOffset; + }, + + minFromRange: function () { + return this.max - this.range; + }, + + /** + * Set the tick positions to round values and optionally extend the extremes + * to the nearest tick + */ + setTickInterval: function (secondPass) { + var axis = this, + chart = axis.chart, + options = axis.options, + isLog = axis.isLog, + log2lin = axis.log2lin, + isDatetimeAxis = axis.isDatetimeAxis, + isXAxis = axis.isXAxis, + isLinked = axis.isLinked, + maxPadding = options.maxPadding, + minPadding = options.minPadding, + length, + linkedParentExtremes, + tickIntervalOption = options.tickInterval, + minTickInterval, + tickPixelIntervalOption = options.tickPixelInterval, + categories = axis.categories, + threshold = axis.threshold, + softThreshold = axis.softThreshold, + thresholdMin, + thresholdMax, + hardMin, + hardMax; + + if (!isDatetimeAxis && !categories && !isLinked) { + this.getTickAmount(); + } + + // Min or max set either by zooming/setExtremes or initial options + hardMin = pick(axis.userMin, options.min); + hardMax = pick(axis.userMax, options.max); + + // Linked axis gets the extremes from the parent axis + if (isLinked) { + axis.linkedParent = chart[axis.coll][options.linkedTo]; + linkedParentExtremes = axis.linkedParent.getExtremes(); + axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin); + axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax); + if (options.type !== axis.linkedParent.options.type) { + error(11, 1); // Can't link axes of different type + } + + // Initial min and max from the extreme data values + } else { + + // Adjust to hard threshold + if (!softThreshold && defined(threshold)) { + if (axis.dataMin >= threshold) { + thresholdMin = threshold; + minPadding = 0; + } else if (axis.dataMax <= threshold) { + thresholdMax = threshold; + maxPadding = 0; + } + } + + axis.min = pick(hardMin, thresholdMin, axis.dataMin); + axis.max = pick(hardMax, thresholdMax, axis.dataMax); + + } + + if (isLog) { + if (!secondPass && mathMin(axis.min, pick(axis.dataMin, axis.min)) <= 0) { // #978 + error(10, 1); // Can't plot negative values on log axis + } + // The correctFloat cures #934, float errors on full tens. But it + // was too aggressive for #4360 because of conversion back to lin, + // therefore use precision 15. + axis.min = correctFloat(log2lin(axis.min), 15); + axis.max = correctFloat(log2lin(axis.max), 15); + } + + // handle zoomed range + if (axis.range && defined(axis.max)) { + axis.userMin = axis.min = hardMin = mathMax(axis.min, axis.minFromRange()); // #618 + axis.userMax = hardMax = axis.max; + + axis.range = null; // don't use it when running setExtremes + } + + // Hook for Highstock Scroller. Consider combining with beforePadding. + fireEvent(axis, 'foundExtremes'); + + // Hook for adjusting this.min and this.max. Used by bubble series. + if (axis.beforePadding) { + axis.beforePadding(); + } + + // adjust min and max for the minimum range + axis.adjustForMinRange(); + + // Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding + // into account, we do this after computing tick interval (#1337). + if (!categories && !axis.axisPointRange && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) { + length = axis.max - axis.min; + if (length) { + if (!defined(hardMin) && minPadding) { + axis.min -= length * minPadding; + } + if (!defined(hardMax) && maxPadding) { + axis.max += length * maxPadding; + } + } + } + + // Stay within floor and ceiling + if (isNumber(options.floor)) { + axis.min = mathMax(axis.min, options.floor); + } + if (isNumber(options.ceiling)) { + axis.max = mathMin(axis.max, options.ceiling); + } + + // When the threshold is soft, adjust the extreme value only if + // the data extreme and the padded extreme land on either side of the threshold. For example, + // a series of [0, 1, 2, 3] would make the yAxis add a tick for -1 because of the + // default minPadding and startOnTick options. This is prevented by the softThreshold + // option. + if (softThreshold && defined(axis.dataMin)) { + threshold = threshold || 0; + if (!defined(hardMin) && axis.min < threshold && axis.dataMin >= threshold) { + axis.min = threshold; + } else if (!defined(hardMax) && axis.max > threshold && axis.dataMax <= threshold) { + axis.max = threshold; + } + } + + + // get tickInterval + if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) { + axis.tickInterval = 1; + } else if (isLinked && !tickIntervalOption && + tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) { + axis.tickInterval = tickIntervalOption = axis.linkedParent.tickInterval; + } else { + axis.tickInterval = pick( + tickIntervalOption, + this.tickAmount ? ((axis.max - axis.min) / mathMax(this.tickAmount - 1, 1)) : undefined, + categories ? // for categoried axis, 1 is default, for linear axis use tickPix + 1 : + // don't let it be more than the data range + (axis.max - axis.min) * tickPixelIntervalOption / mathMax(axis.len, tickPixelIntervalOption) + ); + } + + // Now we're finished detecting min and max, crop and group series data. This + // is in turn needed in order to find tick positions in ordinal axes. + if (isXAxis && !secondPass) { + each(axis.series, function (series) { + series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax); + }); + } + + // set the translation factor used in translate function + axis.setAxisTranslation(true); + + // hook for ordinal axes and radial axes + if (axis.beforeSetTickPositions) { + axis.beforeSetTickPositions(); + } + + // hook for extensions, used in Highstock ordinal axes + if (axis.postProcessTickInterval) { + axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval); + } + + // In column-like charts, don't cramp in more ticks than there are points (#1943, #4184) + if (axis.pointRange && !tickIntervalOption) { + axis.tickInterval = mathMax(axis.pointRange, axis.tickInterval); + } + + // Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined. + minTickInterval = pick(options.minTickInterval, axis.isDatetimeAxis && axis.closestPointRange); + if (!tickIntervalOption && axis.tickInterval < minTickInterval) { + axis.tickInterval = minTickInterval; + } + + // for linear axes, get magnitude and normalize the interval + if (!isDatetimeAxis && !isLog && !tickIntervalOption) { + axis.tickInterval = normalizeTickInterval( + axis.tickInterval, + null, + getMagnitude(axis.tickInterval), + // If the tick interval is between 0.5 and 5 and the axis max is in the order of + // thousands, chances are we are dealing with years. Don't allow decimals. #3363. + pick(options.allowDecimals, !(axis.tickInterval > 0.5 && axis.tickInterval < 5 && axis.max > 1000 && axis.max < 9999)), + !!this.tickAmount + ); + } + + // Prevent ticks from getting so close that we can't draw the labels + if (!this.tickAmount && this.len) { // Color axis with disabled legend has no length + axis.tickInterval = axis.unsquish(); + } + + this.setTickPositions(); + }, + + /** + * Now we have computed the normalized tickInterval, get the tick positions + */ + setTickPositions: function () { + + var options = this.options, + tickPositions, + tickPositionsOption = options.tickPositions, + tickPositioner = options.tickPositioner, + startOnTick = options.startOnTick, + endOnTick = options.endOnTick, + single; + + // Set the tickmarkOffset + this.tickmarkOffset = (this.categories && options.tickmarkPlacement === 'between' && + this.tickInterval === 1) ? 0.5 : 0; // #3202 + + + // get minorTickInterval + this.minorTickInterval = options.minorTickInterval === 'auto' && this.tickInterval ? + this.tickInterval / 5 : options.minorTickInterval; + + // Find the tick positions + this.tickPositions = tickPositions = tickPositionsOption && tickPositionsOption.slice(); // Work on a copy (#1565) + if (!tickPositions) { + + if (this.isDatetimeAxis) { + tickPositions = this.getTimeTicks( + this.normalizeTimeTickInterval(this.tickInterval, options.units), + this.min, + this.max, + options.startOfWeek, + this.ordinalPositions, + this.closestPointRange, + true + ); + } else if (this.isLog) { + tickPositions = this.getLogTickPositions(this.tickInterval, this.min, this.max); + } else { + tickPositions = this.getLinearTickPositions(this.tickInterval, this.min, this.max); + } + + // Too dense ticks, keep only the first and last (#4477) + if (tickPositions.length > this.len) { + tickPositions = [tickPositions[0], tickPositions.pop()]; + } + + this.tickPositions = tickPositions; + + // Run the tick positioner callback, that allows modifying auto tick positions. + if (tickPositioner) { + tickPositioner = tickPositioner.apply(this, [this.min, this.max]); + if (tickPositioner) { + this.tickPositions = tickPositions = tickPositioner; + } + } + + } + + if (!this.isLinked) { + + // reset min/max or remove extremes based on start/end on tick + this.trimTicks(tickPositions, startOnTick, endOnTick); + + // When there is only one point, or all points have the same value on this axis, then min + // and max are equal and tickPositions.length is 0 or 1. In this case, add some padding + // in order to center the point, but leave it with one tick. #1337. + if (this.min === this.max && defined(this.min) && !this.tickAmount) { + // Substract half a unit (#2619, #2846, #2515, #3390) + single = true; + this.min -= 0.5; + this.max += 0.5; + } + this.single = single; + + if (!tickPositionsOption && !tickPositioner) { + this.adjustTickAmount(); + } + } + }, + + /** + * Handle startOnTick and endOnTick by either adapting to padding min/max or rounded min/max + */ + trimTicks: function (tickPositions, startOnTick, endOnTick) { + var roundedMin = tickPositions[0], + roundedMax = tickPositions[tickPositions.length - 1], + minPointOffset = this.minPointOffset || 0; + + if (startOnTick) { + this.min = roundedMin; + } else { + while (this.min - minPointOffset > tickPositions[0]) { + tickPositions.shift(); + } + } + + if (endOnTick) { + this.max = roundedMax; + } else { + while (this.max + minPointOffset < tickPositions[tickPositions.length - 1]) { + tickPositions.pop(); + } + } + + // If no tick are left, set one tick in the middle (#3195) + if (tickPositions.length === 0 && defined(roundedMin)) { + tickPositions.push((roundedMax + roundedMin) / 2); + } + }, + + /** + * Check if there are multiple axes in the same pane + * @returns {Boolean} There are other axes + */ + alignToOthers: function () { + var others = {}, // Whether there is another axis to pair with this one + hasOther, + options = this.options; + + if (this.chart.options.chart.alignTicks !== false && options.alignTicks !== false) { + each(this.chart[this.coll], function (axis) { + var otherOptions = axis.options, + horiz = axis.horiz, + key = [ + horiz ? otherOptions.left : otherOptions.top, + otherOptions.width, + otherOptions.height, + otherOptions.pane + ].join(','); + + + if (axis.series.length) { // #4442 + if (others[key]) { + hasOther = true; // #4201 + } else { + others[key] = 1; + } + } + }); + } + return hasOther; + }, + + /** + * Set the max ticks of either the x and y axis collection + */ + getTickAmount: function () { + var options = this.options, + tickAmount = options.tickAmount, + tickPixelInterval = options.tickPixelInterval; + + if (!defined(options.tickInterval) && this.len < tickPixelInterval && !this.isRadial && + !this.isLog && options.startOnTick && options.endOnTick) { + tickAmount = 2; + } + + if (!tickAmount && this.alignToOthers()) { + // Add 1 because 4 tick intervals require 5 ticks (including first and last) + tickAmount = mathCeil(this.len / tickPixelInterval) + 1; + } + + // For tick amounts of 2 and 3, compute five ticks and remove the intermediate ones. This + // prevents the axis from adding ticks that are too far away from the data extremes. + if (tickAmount < 4) { + this.finalTickAmt = tickAmount; + tickAmount = 5; + } + + this.tickAmount = tickAmount; + }, + + /** + * When using multiple axes, adjust the number of ticks to match the highest + * number of ticks in that group + */ + adjustTickAmount: function () { + var tickInterval = this.tickInterval, + tickPositions = this.tickPositions, + tickAmount = this.tickAmount, + finalTickAmt = this.finalTickAmt, + currentTickAmount = tickPositions && tickPositions.length, + i, + len; + + if (currentTickAmount < tickAmount) { + while (tickPositions.length < tickAmount) { + tickPositions.push(correctFloat( + tickPositions[tickPositions.length - 1] + tickInterval + )); + } + this.transA *= (currentTickAmount - 1) / (tickAmount - 1); + this.max = tickPositions[tickPositions.length - 1]; + + // We have too many ticks, run second pass to try to reduce ticks + } else if (currentTickAmount > tickAmount) { + this.tickInterval *= 2; + this.setTickPositions(); + } + + // The finalTickAmt property is set in getTickAmount + if (defined(finalTickAmt)) { + i = len = tickPositions.length; + while (i--) { + if ( + (finalTickAmt === 3 && i % 2 === 1) || // Remove every other tick + (finalTickAmt <= 2 && i > 0 && i < len - 1) // Remove all but first and last + ) { + tickPositions.splice(i, 1); + } + } + this.finalTickAmt = UNDEFINED; + } + }, + + /** + * Set the scale based on data min and max, user set min and max or options + * + */ + setScale: function () { + var axis = this, + isDirtyData, + isDirtyAxisLength; + + axis.oldMin = axis.min; + axis.oldMax = axis.max; + axis.oldAxisLength = axis.len; + + // set the new axisLength + axis.setAxisSize(); + //axisLength = horiz ? axisWidth : axisHeight; + isDirtyAxisLength = axis.len !== axis.oldAxisLength; + + // is there new data? + each(axis.series, function (series) { + if (series.isDirtyData || series.isDirty || + series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well + isDirtyData = true; + } + }); + + // do we really need to go through all this? + if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw || + axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax || axis.alignToOthers()) { + + if (axis.resetStacks) { + axis.resetStacks(); + } + + axis.forceRedraw = false; + + // get data extremes if needed + axis.getSeriesExtremes(); + + // get fixed positions based on tickInterval + axis.setTickInterval(); + + // record old values to decide whether a rescale is necessary later on (#540) + axis.oldUserMin = axis.userMin; + axis.oldUserMax = axis.userMax; + + // Mark as dirty if it is not already set to dirty and extremes have changed. #595. + if (!axis.isDirty) { + axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax; + } + } else if (axis.cleanStacks) { + axis.cleanStacks(); + } + }, + + /** + * 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 + * @param {Object} eventArguments + * + */ + setExtremes: function (newMin, newMax, redraw, animation, eventArguments) { + var axis = this, + chart = axis.chart; + + redraw = pick(redraw, true); // defaults to true + + each(axis.series, function (serie) { + delete serie.kdTree; + }); + + // Extend the arguments with min and max + eventArguments = extend(eventArguments, { + min: newMin, + max: newMax + }); + + // Fire the event + fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler + + axis.userMin = newMin; + axis.userMax = newMax; + axis.eventArgs = eventArguments; + + if (redraw) { + chart.redraw(animation); + } + }); + }, + + /** + * Overridable method for zooming chart. Pulled out in a separate method to allow overriding + * in stock charts. + */ + zoom: function (newMin, newMax) { + var dataMin = this.dataMin, + dataMax = this.dataMax, + options = this.options, + min = mathMin(dataMin, pick(options.min, dataMin)), + max = mathMax(dataMax, pick(options.max, dataMax)); + + // Prevent pinch zooming out of range. Check for defined is for #1946. #1734. + if (!this.allowZoomOutside) { + if (defined(dataMin) && newMin <= min) { + newMin = min; + } + if (defined(dataMax) && newMax >= max) { + newMax = max; + } + } + + // In full view, displaying the reset zoom button is not required + this.displayBtn = newMin !== UNDEFINED || newMax !== UNDEFINED; + + // Do it + this.setExtremes( + newMin, + newMax, + false, + UNDEFINED, + { trigger: 'zoom' } + ); + return true; + }, + + /** + * Update the axis metrics + */ + setAxisSize: function () { + var chart = this.chart, + options = this.options, + offsetLeft = options.offsetLeft || 0, + offsetRight = options.offsetRight || 0, + horiz = this.horiz, + width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight), + height = pick(options.height, chart.plotHeight), + top = pick(options.top, chart.plotTop), + left = pick(options.left, chart.plotLeft + offsetLeft), + percentRegex = /%$/; + + // Check for percentage based input values. Rounding fixes problems with + // column overflow and plot line filtering (#4898, #4899) + if (percentRegex.test(height)) { + height = Math.round(parseFloat(height) / 100 * chart.plotHeight); + } + if (percentRegex.test(top)) { + top = Math.round(parseFloat(top) / 100 * chart.plotHeight + chart.plotTop); + } + + // Expose basic values to use in Series object and navigator + this.left = left; + this.top = top; + this.width = width; + this.height = height; + this.bottom = chart.chartHeight - height - top; + this.right = chart.chartWidth - width - left; + + // Direction agnostic properties + this.len = mathMax(horiz ? width : height, 0); // mathMax fixes #905 + this.pos = horiz ? left : top; // distance from SVG origin + }, + + /** + * Get the actual axis extremes + */ + getExtremes: function () { + var axis = this, + isLog = axis.isLog, + lin2log = axis.lin2log; + + return { + min: isLog ? correctFloat(lin2log(axis.min)) : axis.min, + max: isLog ? correctFloat(lin2log(axis.max)) : axis.max, + dataMin: axis.dataMin, + dataMax: axis.dataMax, + userMin: axis.userMin, + userMax: axis.userMax + }; + }, + + /** + * Get the zero plane either based on zero or on the min or max value. + * Used in bar and area plots + */ + getThreshold: function (threshold) { + var axis = this, + isLog = axis.isLog, + lin2log = axis.lin2log, + realMin = isLog ? lin2log(axis.min) : axis.min, + realMax = isLog ? lin2log(axis.max) : axis.max; + + // With a threshold of null, make the columns/areas rise from the top or bottom + // depending on the value, assuming an actual threshold of 0 (#4233). + if (threshold === null) { + threshold = realMax < 0 ? realMax : realMin; + } else if (realMin > threshold) { + threshold = realMin; + } else if (realMax < threshold) { + threshold = realMax; + } + + return axis.translate(threshold, 0, 1, 0, 1); + }, + + /** + * Compute auto alignment for the axis label based on which side the axis is on + * and the given rotation for the label + */ + autoLabelAlign: function (rotation) { + var ret, + angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360; + + if (angle > 15 && angle < 165) { + ret = 'right'; + } else if (angle > 195 && angle < 345) { + ret = 'left'; + } else { + ret = 'center'; + } + return ret; + }, + + /** + * Get the tick length and width for the axis. + * @param {String} prefix 'tick' or 'minorTick' + * @returns {Array} An array of tickLength and tickWidth + */ + tickSize: function (prefix) { + var options = this.options, + tickLength = options[prefix + 'Length'], + tickWidth = pick(options[prefix + 'Width'], prefix === 'tick' && this.isXAxis ? 1 : 0); // X axis defaults to 1 + + if (tickWidth && tickLength) { + // Negate the length + if (options[prefix + 'Position'] === 'inside') { + tickLength = -tickLength; + } + return [tickLength, tickWidth]; + } + + }, + + /** + * Return the size of the labels + */ + labelMetrics: function () { + return this.chart.renderer.fontMetrics( + this.options.labels.style.fontSize, + this.ticks[0] && this.ticks[0].label + ); + }, + + /** + * Prevent the ticks from getting so close we can't draw the labels. On a horizontal + * axis, this is handled by rotating the labels, removing ticks and adding ellipsis. + * On a vertical axis remove ticks and add ellipsis. + */ + unsquish: function () { + var labelOptions = this.options.labels, + horiz = this.horiz, + tickInterval = this.tickInterval, + newTickInterval = tickInterval, + slotSize = this.len / (((this.categories ? 1 : 0) + this.max - this.min) / tickInterval), + rotation, + rotationOption = labelOptions.rotation, + labelMetrics = this.labelMetrics(), + step, + bestScore = Number.MAX_VALUE, + autoRotation, + // Return the multiple of tickInterval that is needed to avoid collision + getStep = function (spaceNeeded) { + var step = spaceNeeded / (slotSize || 1); + step = step > 1 ? mathCeil(step) : 1; + return step * tickInterval; + }; + + if (horiz) { + autoRotation = !labelOptions.staggerLines && !labelOptions.step && ( // #3971 + defined(rotationOption) ? + [rotationOption] : + slotSize < pick(labelOptions.autoRotationLimit, 80) && labelOptions.autoRotation + ); + + if (autoRotation) { + + // Loop over the given autoRotation options, and determine which gives the best score. The + // best score is that with the lowest number of steps and a rotation closest to horizontal. + each(autoRotation, function (rot) { + var score; + + if (rot === rotationOption || (rot && rot >= -90 && rot <= 90)) { // #3891 + + step = getStep(mathAbs(labelMetrics.h / mathSin(deg2rad * rot))); + + score = step + mathAbs(rot / 360); + + if (score < bestScore) { + bestScore = score; + rotation = rot; + newTickInterval = step; + } + } + }); + } + + } else if (!labelOptions.step) { // #4411 + newTickInterval = getStep(labelMetrics.h); + } + + this.autoRotation = autoRotation; + this.labelRotation = pick(rotation, rotationOption); + + return newTickInterval; + }, + + /** + * Get the general slot width for this axis. This may change between the pre-render (from Axis.getOffset) + * and the final tick rendering and placement (#5086). + */ + getSlotWidth: function () { + var chart = this.chart, + horiz = this.horiz, + labelOptions = this.options.labels, + slotCount = Math.max(this.tickPositions.length - (this.categories ? 0 : 1), 1), + marginLeft = chart.margin[3]; + + return (horiz && (labelOptions.step || 0) < 2 && !labelOptions.rotation && // #4415 + ((this.staggerLines || 1) * chart.plotWidth) / slotCount) || + (!horiz && ((marginLeft && (marginLeft - chart.spacing[3])) || chart.chartWidth * 0.33)); // #1580, #1931 + + }, + + /** + * Render the axis labels and determine whether ellipsis or rotation need to be applied + */ + renderUnsquish: function () { + var chart = this.chart, + renderer = chart.renderer, + tickPositions = this.tickPositions, + ticks = this.ticks, + labelOptions = this.options.labels, + horiz = this.horiz, + slotWidth = this.getSlotWidth(), + innerWidth = mathMax(1, mathRound(slotWidth - 2 * (labelOptions.padding || 5))), + attr = {}, + labelMetrics = this.labelMetrics(), + textOverflowOption = labelOptions.style.textOverflow, + css, + labelLength = 0, + label, + i, + pos; + + // Set rotation option unless it is "auto", like in gauges + if (!isString(labelOptions.rotation)) { + attr.rotation = labelOptions.rotation || 0; // #4443 + } + + // Handle auto rotation on horizontal axis + if (this.autoRotation) { + + // Get the longest label length + each(tickPositions, function (tick) { + tick = ticks[tick]; + if (tick && tick.labelLength > labelLength) { + labelLength = tick.labelLength; + } + }); + + // Apply rotation only if the label is too wide for the slot, and + // the label is wider than its height. + if (labelLength > innerWidth && labelLength > labelMetrics.h) { + attr.rotation = this.labelRotation; + } else { + this.labelRotation = 0; + } + + // Handle word-wrap or ellipsis on vertical axis + } else if (slotWidth) { + // For word-wrap or ellipsis + css = { width: innerWidth + PX }; + + if (!textOverflowOption) { + css.textOverflow = 'clip'; + + // On vertical axis, only allow word wrap if there is room for more lines. + i = tickPositions.length; + while (!horiz && i--) { + pos = tickPositions[i]; + label = ticks[pos].label; + if (label) { + // Reset ellipsis in order to get the correct bounding box (#4070) + if (label.styles.textOverflow === 'ellipsis') { + label.css({ textOverflow: 'clip' }); + + // Set the correct width in order to read the bounding box height (#4678, #5034) + } else if (ticks[pos].labelLength > slotWidth) { + label.css({ width: slotWidth + 'px' }); + } + + if (label.getBBox().height > this.len / tickPositions.length - (labelMetrics.h - labelMetrics.f)) { + label.specCss = { textOverflow: 'ellipsis' }; + } + } + } + } + } + + + // Add ellipsis if the label length is significantly longer than ideal + if (attr.rotation) { + css = { + width: (labelLength > chart.chartHeight * 0.5 ? chart.chartHeight * 0.33 : chart.chartHeight) + PX + }; + if (!textOverflowOption) { + css.textOverflow = 'ellipsis'; + } + } + + // Set the explicit or automatic label alignment + this.labelAlign = labelOptions.align || this.autoLabelAlign(this.labelRotation); + if (this.labelAlign) { + attr.align = this.labelAlign; + } + + // Apply general and specific CSS + each(tickPositions, function (pos) { + var tick = ticks[pos], + label = tick && tick.label; + if (label) { + label.attr(attr); // This needs to go before the CSS in old IE (#4502) + if (css) { + label.css(merge(css, label.specCss)); + } + delete label.specCss; + tick.rotation = attr.rotation; + } + }); + + // Note: Why is this not part of getLabelPosition? + this.tickRotCorr = renderer.rotCorr(labelMetrics.b, this.labelRotation || 0, this.side !== 0); + }, + + /** + * Return true if the axis has associated data + */ + hasData: function () { + return this.hasVisibleSeries || (defined(this.min) && defined(this.max) && !!this.tickPositions); + }, + + /** + * Render the tick labels to a preliminary position to get their sizes + */ + getOffset: function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + options = axis.options, + tickPositions = axis.tickPositions, + ticks = axis.ticks, + horiz = axis.horiz, + side = axis.side, + invertedSide = chart.inverted ? [1, 0, 3, 2][side] : side, + hasData, + showAxis, + titleOffset = 0, + titleOffsetOption, + titleMargin = 0, + axisTitleOptions = options.title, + labelOptions = options.labels, + labelOffset = 0, // reset + labelOffsetPadded, + opposite = axis.opposite, + axisOffset = chart.axisOffset, + clipOffset = chart.clipOffset, + clip, + directionFactor = [-1, 1, 1, -1][side], + n, + textAlign, + axisParent = axis.axisParent, // Used in color axis + lineHeightCorrection, + tickSize = this.tickSize('tick'); + + // For reuse in Axis.render + hasData = axis.hasData(); + axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); + + // Set/reset staggerLines + axis.staggerLines = axis.horiz && labelOptions.staggerLines; + + // Create the axisGroup and gridGroup elements on first iteration + if (!axis.axisGroup) { + axis.gridGroup = renderer.g('grid') + .attr({ zIndex: options.gridZIndex || 1 }) + .add(axisParent); + axis.axisGroup = renderer.g('axis') + .attr({ zIndex: options.zIndex || 2 }) + .add(axisParent); + axis.labelGroup = renderer.g('axis-labels') + .attr({ zIndex: labelOptions.zIndex || 7 }) + .addClass(PREFIX + axis.coll.toLowerCase() + '-labels') + .add(axisParent); + } + + if (hasData || axis.isLinked) { + + // Generate ticks + each(tickPositions, function (pos) { + if (!ticks[pos]) { + ticks[pos] = new Tick(axis, pos); + } else { + ticks[pos].addLabel(); // update labels depending on tick interval + } + }); + + axis.renderUnsquish(); + + + // Left side must be align: right and right side must have align: left for labels + if (labelOptions.reserveSpace !== false && (side === 0 || side === 2 || + { 1: 'left', 3: 'right' }[side] === axis.labelAlign || axis.labelAlign === 'center')) { + each(tickPositions, function (pos) { + + // get the highest offset + labelOffset = mathMax( + ticks[pos].getLabelSize(), + labelOffset + ); + }); + } + + if (axis.staggerLines) { + labelOffset *= axis.staggerLines; + axis.labelOffset = labelOffset * (axis.opposite ? -1 : 1); + } + + + } else { // doesn't have data + for (n in ticks) { + ticks[n].destroy(); + delete ticks[n]; + } + } + + if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) { + if (!axis.axisTitle) { + textAlign = axisTitleOptions.textAlign; + if (!textAlign) { + textAlign = (horiz ? { + low: 'left', + middle: 'center', + high: 'right' + } : { + low: opposite ? 'right' : 'left', + middle: 'center', + high: opposite ? 'left' : 'right' + })[axisTitleOptions.align]; + } + axis.axisTitle = renderer.text( + axisTitleOptions.text, + 0, + 0, + axisTitleOptions.useHTML + ) + .attr({ + zIndex: 7, + rotation: axisTitleOptions.rotation || 0, + align: textAlign + }) + .addClass(PREFIX + this.coll.toLowerCase() + '-title') + .css(axisTitleOptions.style) + .add(axis.axisGroup); + axis.axisTitle.isNew = true; + } + + if (showAxis) { + titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width']; + titleOffsetOption = axisTitleOptions.offset; + titleMargin = defined(titleOffsetOption) ? 0 : pick(axisTitleOptions.margin, horiz ? 5 : 10); + } + + // hide or show the title depending on whether showEmpty is set + axis.axisTitle[showAxis ? 'show' : 'hide'](true); + } + + // handle automatic or user set offset + axis.offset = directionFactor * pick(options.offset, axisOffset[side]); + + axis.tickRotCorr = axis.tickRotCorr || { x: 0, y: 0 }; // polar + if (side === 0) { + lineHeightCorrection = -axis.labelMetrics().h; + } else if (side === 2) { + lineHeightCorrection = axis.tickRotCorr.y; + } else { + lineHeightCorrection = 0; + } + + // Find the padded label offset + labelOffsetPadded = Math.abs(labelOffset) + titleMargin; + if (labelOffset) { + labelOffsetPadded -= lineHeightCorrection; + labelOffsetPadded += directionFactor * (horiz ? pick(labelOptions.y, axis.tickRotCorr.y + directionFactor * 8) : labelOptions.x); + } + axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded); + + axisOffset[side] = mathMax( + axisOffset[side], + axis.axisTitleMargin + titleOffset + directionFactor * axis.offset, + labelOffsetPadded, // #3027 + hasData && tickPositions.length && tickSize ? tickSize[0] : 0 // #4866 + ); + + // Decide the clipping needed to keep the graph inside the plot area and axis lines + clip = options.offset ? 0 : mathFloor(options.lineWidth / 2) * 2; // #4308, #4371 + clipOffset[invertedSide] = mathMax(clipOffset[invertedSide], clip); + }, + + /** + * Get the path for the axis line + */ + getLinePath: function (lineWidth) { + var chart = this.chart, + opposite = this.opposite, + offset = this.offset, + horiz = this.horiz, + lineLeft = this.left + (opposite ? this.width : 0) + offset, + lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset; + + if (opposite) { + lineWidth *= -1; // crispify the other way - #1480, #1687 + } + + return chart.renderer + .crispLine([ + M, + horiz ? + this.left : + lineLeft, + horiz ? + lineTop : + this.top, + L, + horiz ? + chart.chartWidth - this.right : + lineLeft, + horiz ? + lineTop : + chart.chartHeight - this.bottom + ], lineWidth); + }, + + /** + * Position the title + */ + getTitlePosition: function () { + // compute anchor points for each of the title align options + var horiz = this.horiz, + axisLeft = this.left, + axisTop = this.top, + axisLength = this.len, + axisTitleOptions = this.options.title, + margin = horiz ? axisLeft : axisTop, + opposite = this.opposite, + offset = this.offset, + xOption = axisTitleOptions.x || 0, + yOption = axisTitleOptions.y || 0, + 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 ? axisTop + this.height : axisLeft) + + (horiz ? 1 : -1) * // horizontal axis reverses the margin + (opposite ? -1 : 1) * // so does opposite axes + this.axisTitleMargin + + (this.side === 2 ? fontSize : 0); + + return { + x: horiz ? + alongAxis + xOption : + offAxis + (opposite ? this.width : 0) + offset + xOption, + y: horiz ? + offAxis + yOption - (opposite ? this.height : 0) + offset : + alongAxis + yOption + }; + }, + + /** + * Render the axis + */ + render: function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + options = axis.options, + isLog = axis.isLog, + lin2log = axis.lin2log, + isLinked = axis.isLinked, + tickPositions = axis.tickPositions, + axisTitle = axis.axisTitle, + ticks = axis.ticks, + minorTicks = axis.minorTicks, + alternateBands = axis.alternateBands, + stackLabelOptions = options.stackLabels, + alternateGridColor = options.alternateGridColor, + tickmarkOffset = axis.tickmarkOffset, + lineWidth = options.lineWidth, + linePath, + hasRendered = chart.hasRendered, + slideInTicks = hasRendered && isNumber(axis.oldMin), + showAxis = axis.showAxis, + animation = animObject(renderer.globalAnimation), + from, + to; + + // Reset + axis.labelEdge.length = 0; + //axis.justifyToPlot = overflow === 'justify'; + axis.overlap = false; + + // Mark all elements inActive before we go over and mark the active ones + each([ticks, minorTicks, alternateBands], function (coll) { + var pos; + for (pos in coll) { + coll[pos].isActive = false; + } + }); + + // If the series has data draw the ticks. Else only the line and title + if (axis.hasData() || isLinked) { + + // minor ticks + if (axis.minorTickInterval && !axis.categories) { + each(axis.getMinorTickPositions(), function (pos) { + if (!minorTicks[pos]) { + minorTicks[pos] = new Tick(axis, pos, 'minor'); + } + + // render new ticks in old position + if (slideInTicks && minorTicks[pos].isNew) { + minorTicks[pos].render(null, true); + } + + minorTicks[pos].render(null, false, 1); + }); + } + + // Major ticks. Pull out the first item and render it last so that + // we can get the position of the neighbour label. #808. + if (tickPositions.length) { // #1300 + each(tickPositions, function (pos, i) { + + // linked axes need an extra check to find out if + if (!isLinked || (pos >= axis.min && pos <= axis.max)) { + + if (!ticks[pos]) { + ticks[pos] = new Tick(axis, pos); + } + + // render new ticks in old position + if (slideInTicks && ticks[pos].isNew) { + ticks[pos].render(i, true, 0.1); + } + + ticks[pos].render(i); + } + + }); + // In a categorized axis, the tick marks are displayed between labels. So + // we need to add a tick mark and grid line at the left edge of the X axis. + if (tickmarkOffset && (axis.min === 0 || axis.single)) { + if (!ticks[-1]) { + ticks[-1] = new Tick(axis, -1, null, true); + } + ticks[-1].render(-1); + } + + } + + // alternate grid color + if (alternateGridColor) { + each(tickPositions, function (pos, i) { + to = tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] + tickmarkOffset : axis.max - tickmarkOffset; + if (i % 2 === 0 && pos < axis.max && to <= axis.max + (chart.polar ? -tickmarkOffset : tickmarkOffset)) { // #2248, #4660 + if (!alternateBands[pos]) { + alternateBands[pos] = new Highcharts.PlotLineOrBand(axis); + } + from = pos + tickmarkOffset; // #949 + alternateBands[pos].options = { + from: isLog ? lin2log(from) : from, + to: isLog ? lin2log(to) : to, + color: alternateGridColor + }; + alternateBands[pos].render(); + alternateBands[pos].isActive = true; + } + }); + } + + // custom plot lines and bands + if (!axis._addedPlotLB) { // only first time + each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) { + axis.addPlotBandOrLine(plotLineOptions); + }); + axis._addedPlotLB = true; + } + + } // end if hasData + + // Remove inactive ticks + each([ticks, minorTicks, alternateBands], function (coll) { + var pos, + i, + forDestruction = [], + delay = animation.duration, + destroyInactiveItems = function () { + i = forDestruction.length; + while (i--) { + // When resizing rapidly, the same items may be destroyed in different timeouts, + // or the may be reactivated + if (coll[forDestruction[i]] && !coll[forDestruction[i]].isActive) { + coll[forDestruction[i]].destroy(); + delete coll[forDestruction[i]]; + } + } + + }; + + for (pos in coll) { + + if (!coll[pos].isActive) { + // Render to zero opacity + coll[pos].render(pos, false, 0); + coll[pos].isActive = false; + forDestruction.push(pos); + } + } + + // When the objects are finished fading out, destroy them + syncTimeout( + destroyInactiveItems, + coll === alternateBands || !chart.hasRendered || !delay ? 0 : delay + ); + }); + + // Static items. As the axis group is cleared on subsequent calls + // to render, these items are added outside the group. + // axis line + if (lineWidth) { + linePath = axis.getLinePath(lineWidth); + if (!axis.axisLine) { + axis.axisLine = renderer.path(linePath) + .attr({ + stroke: options.lineColor, + 'stroke-width': lineWidth, + zIndex: 7 + }) + .add(axis.axisGroup); + } else { + axis.axisLine.animate({ d: linePath }); + } + + // show or hide the line depending on options.showEmpty + axis.axisLine[showAxis ? 'show' : 'hide'](true); + } + + if (axisTitle && showAxis) { + + axisTitle[axisTitle.isNew ? 'attr' : 'animate']( + axis.getTitlePosition() + ); + axisTitle.isNew = false; + } + + // Stacked totals: + if (stackLabelOptions && stackLabelOptions.enabled) { + axis.renderStackTotals(); + } + // End stacked totals + + axis.isDirty = false; + }, + + /** + * Redraw the axis to reflect changes in the data or axis extremes + */ + redraw: function () { + + if (this.visible) { + // render the axis + this.render(); + + // move plot lines and bands + each(this.plotLinesAndBands, function (plotLine) { + plotLine.render(); + }); + } + + // mark associated series as dirty and ready for redraw + each(this.series, function (series) { + series.isDirty = true; + }); + + }, + + /** + * Destroys an Axis instance. + */ + destroy: function (keepEvents) { + var axis = this, + stacks = axis.stacks, + stackKey, + plotLinesAndBands = axis.plotLinesAndBands, + i; + + // Remove the events + if (!keepEvents) { + removeEvent(axis); + } + + // Destroy each stack total + for (stackKey in stacks) { + destroyObjectProperties(stacks[stackKey]); + + stacks[stackKey] = null; + } + + // Destroy collections + each([axis.ticks, axis.minorTicks, axis.alternateBands], function (coll) { + destroyObjectProperties(coll); + }); + i = plotLinesAndBands.length; + while (i--) { // #1975 + plotLinesAndBands[i].destroy(); + } + + // Destroy properties + each(['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', 'gridGroup', 'labelGroup', 'cross'], function (prop) { + if (axis[prop]) { + axis[prop] = axis[prop].destroy(); + } + }); + + + this._addedPlotLB = this.chart._labelPanes = this.ordinalSlope = undefined; // #1611, #2887, #4314, #5316 + }, + + /** + * Draw the crosshair + * + * @param {Object} e The event arguments from the modified pointer event + * @param {Object} point The Point object + */ + drawCrosshair: function (e, point) { + + var path, + options = this.crosshair, + pos, + attribs, + categorized, + strokeWidth; + + if ( + // Disabled in options + !this.crosshair || + // Snap + ((defined(point) || !pick(options.snap, true)) === false) + ) { + this.hideCrosshair(); + + } else { + + // Get the path + if (!pick(options.snap, true)) { + pos = (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos); + } else if (defined(point)) { + pos = this.isXAxis ? point.plotX : this.len - point.plotY; // #3834 + } + + if (this.isRadial) { + path = this.getPlotLinePath(this.isXAxis ? point.x : pick(point.stackY, point.y)) || null; // #3189 + } else { + path = this.getPlotLinePath(null, null, null, null, pos) || null; // #3189 + } + + if (path === null) { + this.hideCrosshair(); + return; + } + + categorized = this.categories && !this.isRadial; + strokeWidth = pick(options.width, (categorized ? this.transA : 1)); + + // Draw the cross + if (this.cross) { + this.cross + .attr({ + d: path, + visibility: 'visible', + 'stroke-width': strokeWidth // #4737 + }); + } else { + attribs = { + 'pointer-events': 'none', // #5259 + 'stroke-width': strokeWidth, + stroke: options.color || (categorized ? 'rgba(155,200,255,0.2)' : '#C0C0C0'), + zIndex: pick(options.zIndex, 2) + }; + if (options.dashStyle) { + attribs.dashstyle = options.dashStyle; + } + this.cross = this.chart.renderer.path(path).attr(attribs).add(); + } + + } + + }, + + /** + * Hide the crosshair. + */ + hideCrosshair: function () { + if (this.cross) { + this.cross.hide(); + } + } + }; // end Axis + + extend(Axis.prototype, AxisPlotLineOrBandExtension); + + /** + * Set the tick positions to a time unit that makes sense, for example + * on the first of each month or on every Monday. Return an array + * with the time positions. Used in datetime axes as well as for grouping + * data on a datetime axis. + * + * @param {Object} normalizedInterval The interval in axis values (ms) and the count + * @param {Number} min The minimum in axis values + * @param {Number} max The maximum in axis values + * @param {Number} startOfWeek + */ + Axis.prototype.getTimeTicks = function (normalizedInterval, min, max, startOfWeek) { + var tickPositions = [], + i, + higherRanks = {}, + useUTC = defaultOptions.global.useUTC, + minYear, // used in months and years as a basis for Date.UTC() + minDate = new Date(min - getTZOffset(min)), + interval = normalizedInterval.unitRange, + count = normalizedInterval.count; + + if (defined(min)) { // #1300 + minDate[setMilliseconds](interval >= timeUnits.second ? 0 : // #3935 + count * mathFloor(minDate.getMilliseconds() / count)); // #3652, #3654 + + if (interval >= timeUnits.second) { // second + minDate[setSeconds](interval >= timeUnits.minute ? 0 : // #3935 + count * mathFloor(minDate.getSeconds() / count)); + } + + if (interval >= timeUnits.minute) { // minute + minDate[setMinutes](interval >= timeUnits.hour ? 0 : + count * mathFloor(minDate[getMinutes]() / count)); + } + + if (interval >= timeUnits.hour) { // hour + minDate[setHours](interval >= timeUnits.day ? 0 : + count * mathFloor(minDate[getHours]() / count)); + } + + if (interval >= timeUnits.day) { // day + minDate[setDate](interval >= timeUnits.month ? 1 : + count * mathFloor(minDate[getDate]() / count)); + } + + if (interval >= timeUnits.month) { // month + minDate[setMonth](interval >= timeUnits.year ? 0 : + count * mathFloor(minDate[getMonth]() / count)); + minYear = minDate[getFullYear](); + } + + if (interval >= timeUnits.year) { // year + minYear -= minYear % count; + minDate[setFullYear](minYear); + } + + // week is a special case that runs outside the hierarchy + if (interval === timeUnits.week) { + // get start of current week, independent of count + minDate[setDate](minDate[getDate]() - minDate[getDay]() + + pick(startOfWeek, 1)); + } + + + // get tick positions + i = 1; + if (timezoneOffset || getTimezoneOffset) { + minDate = minDate.getTime(); + minDate = new Date(minDate + getTZOffset(minDate)); + } + minYear = minDate[getFullYear](); + var time = minDate.getTime(), + minMonth = minDate[getMonth](), + minDateDate = minDate[getDate](), + variableDayLength = !useUTC || !!getTimezoneOffset, // #4951 + localTimezoneOffset = (timeUnits.day + + (useUTC ? getTZOffset(minDate) : minDate.getTimezoneOffset() * 60 * 1000) + ) % timeUnits.day; // #950, #3359 + + // iterate and add tick positions at appropriate values + while (time < max) { + tickPositions.push(time); + + // if the interval is years, use Date.UTC to increase years + if (interval === timeUnits.year) { + time = makeTime(minYear + i * count, 0); + + // if the interval is months, use Date.UTC to increase months + } else if (interval === timeUnits.month) { + time = makeTime(minYear, minMonth + i * count); + + // if we're using global time, the interval is not fixed as it jumps + // one hour at the DST crossover + } else if (variableDayLength && (interval === timeUnits.day || interval === timeUnits.week)) { + time = makeTime(minYear, minMonth, minDateDate + + i * count * (interval === timeUnits.day ? 1 : 7)); + + // else, the interval is fixed and we use simple addition + } else { + time += interval * count; + } + + i++; + } + + // push the last time + tickPositions.push(time); + + + // mark new days if the time is dividible by day (#1649, #1760) + each(grep(tickPositions, function (time) { + return interval <= timeUnits.hour && time % timeUnits.day === localTimezoneOffset; + }), function (time) { + higherRanks[time] = 'day'; + }); + } + + + // record information on the chosen unit - for dynamic label formatter + tickPositions.info = extend(normalizedInterval, { + higherRanks: higherRanks, + totalRange: interval * count + }); + + return tickPositions; + }; + + /** + * Get a normalized tick interval for dates. Returns a configuration object with + * unit range (interval), count and name. Used to prepare data for getTimeTicks. + * Previously this logic was part of getTimeTicks, but as getTimeTicks now runs + * of segments in stock charts, the normalizing logic was extracted in order to + * prevent it for running over again for each segment having the same interval. + * #662, #697. + */ + Axis.prototype.normalizeTimeTickInterval = function (tickInterval, unitsOption) { + var units = unitsOption || [[ + 'millisecond', // unit name + [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples + ], [ + 'second', + [1, 2, 5, 10, 15, 30] + ], [ + 'minute', + [1, 2, 5, 10, 15, 30] + ], [ + 'hour', + [1, 2, 3, 4, 6, 8, 12] + ], [ + 'day', + [1, 2] + ], [ + 'week', + [1, 2] + ], [ + 'month', + [1, 2, 3, 4, 6] + ], [ + 'year', + null + ]], + unit = units[units.length - 1], // default unit is years + interval = timeUnits[unit[0]], + multiples = unit[1], + count, + i; + + // loop through the units to find the one that best fits the tickInterval + for (i = 0; i < units.length; i++) { + unit = units[i]; + interval = timeUnits[unit[0]]; + multiples = unit[1]; + + + if (units[i + 1]) { + // lessThan is in the middle between the highest multiple and the next unit. + var lessThan = (interval * multiples[multiples.length - 1] + + timeUnits[units[i + 1][0]]) / 2; + + // break and keep the current unit + if (tickInterval <= lessThan) { + break; + } + } + } + + // prevent 2.5 years intervals, though 25, 250 etc. are allowed + if (interval === timeUnits.year && tickInterval < 5 * interval) { + multiples = [1, 2, 5]; + } + + // get the count + count = normalizeTickInterval( + tickInterval / interval, + multiples, + unit[0] === 'year' ? mathMax(getMagnitude(tickInterval / interval), 1) : 1 // #1913, #2360 + ); + + return { + unitRange: interval, + count: count, + unitName: unit[0] + }; + }; + /** + * Methods defined on the Axis prototype + */ + + /** + * Set the tick positions of a logarithmic axis + */ + Axis.prototype.getLogTickPositions = function (interval, min, max, minor) { + var axis = this, + options = axis.options, + axisLength = axis.len, + lin2log = axis.lin2log, + log2lin = axis.log2lin, + // Since we use this method for both major and minor ticks, + // use a local variable and return the result + positions = []; + + // Reset + if (!minor) { + axis._minorAutoInterval = null; + } + + // First case: All ticks fall on whole logarithms: 1, 10, 100 etc. + if (interval >= 0.5) { + interval = mathRound(interval); + positions = axis.getLinearTickPositions(interval, min, max); + + // Second case: We need intermediary ticks. For example + // 1, 2, 4, 6, 8, 10, 20, 40 etc. + } else if (interval >= 0.08) { + var roundedMin = mathFloor(min), + intermediate, + i, + j, + len, + pos, + lastPos, + break2; + + if (interval > 0.3) { + intermediate = [1, 2, 4]; + } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc + intermediate = [1, 2, 4, 6, 8]; + } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc + intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + } + + for (i = roundedMin; i < max + 1 && !break2; i++) { + len = intermediate.length; + for (j = 0; j < len && !break2; j++) { + pos = log2lin(lin2log(i) * intermediate[j]); + if (pos > min && (!minor || lastPos <= max) && lastPos !== UNDEFINED) { // #1670, lastPos is #3113 + positions.push(lastPos); + } + + if (lastPos > max) { + break2 = true; + } + lastPos = pos; + } + } + + // Third case: We are so deep in between whole logarithmic values that + // we might as well handle the tick positions like a linear axis. For + // example 1.01, 1.02, 1.03, 1.04. + } else { + var realMin = lin2log(min), + realMax = lin2log(max), + tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'], + filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption, + tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1), + totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength; + + interval = pick( + filteredTickIntervalOption, + axis._minorAutoInterval, + (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1) + ); + + interval = normalizeTickInterval( + interval, + null, + getMagnitude(interval) + ); + + positions = map(axis.getLinearTickPositions( + interval, + realMin, + realMax + ), log2lin); + + if (!minor) { + axis._minorAutoInterval = interval / 5; + } + } + + // Set the axis-level tickInterval variable + if (!minor) { + axis.tickInterval = interval; + } + return positions; + }; + + Axis.prototype.log2lin = function (num) { + return math.log(num) / math.LN10; + }; + + Axis.prototype.lin2log = function (num) { + return math.pow(10, num); + }; + /** + * The tooltip object + * @param {Object} chart The chart instance + * @param {Object} options Tooltip options + */ + var Tooltip = Highcharts.Tooltip = function () { + this.init.apply(this, arguments); + }; + + Tooltip.prototype = { + + init: function (chart, options) { + + var borderWidth = options.borderWidth, + style = options.style, + padding = pInt(style.padding); + + // Save the chart and options + this.chart = chart; + this.options = options; + + // Keep track of the current series + //this.currentSeries = UNDEFINED; + + // List of crosshairs + this.crosshairs = []; + + // Current values of x and y when animating + this.now = { x: 0, y: 0 }; + + // The tooltip is initially hidden + this.isHidden = true; + + + // create the label + this.label = chart.renderer.label('', 0, 0, options.shape || 'callout', null, null, options.useHTML, null, 'tooltip') + .attr({ + padding: padding, + fill: options.backgroundColor, + 'stroke-width': borderWidth, + r: options.borderRadius, + zIndex: 8 + }) + .css(style) + .css({ padding: 0 }) // Remove it from VML, the padding is applied as an attribute instead (#1117) + .add() + .attr({ y: -9999 }); // #2301, #2657 + + // When using canVG the shadow shows up as a gray circle + // even if the tooltip is hidden. + if (!useCanVG) { + this.label.shadow(options.shadow); + } + + // Public property for getting the shared state. + this.shared = options.shared; + }, + + /** + * Destroy the tooltip and its elements. + */ + destroy: function () { + // Destroy and clear local variables + if (this.label) { + this.label = this.label.destroy(); + } + clearTimeout(this.hideTimer); + clearTimeout(this.tooltipTimeout); + }, + + /** + * Provide a soft movement for the tooltip + * + * @param {Number} x + * @param {Number} y + * @private + */ + move: function (x, y, anchorX, anchorY) { + var tooltip = this, + now = tooltip.now, + animate = tooltip.options.animation !== false && !tooltip.isHidden && + // When we get close to the target position, abort animation and land on the right place (#3056) + (mathAbs(x - now.x) > 1 || mathAbs(y - now.y) > 1), + skipAnchor = tooltip.followPointer || tooltip.len > 1; + + // Get intermediate values for animation + extend(now, { + x: animate ? (2 * now.x + x) / 3 : x, + y: animate ? (now.y + y) / 2 : y, + anchorX: skipAnchor ? UNDEFINED : animate ? (2 * now.anchorX + anchorX) / 3 : anchorX, + anchorY: skipAnchor ? UNDEFINED : animate ? (now.anchorY + anchorY) / 2 : anchorY + }); + + // Move to the intermediate value + tooltip.label.attr(now); + + + // Run on next tick of the mouse tracker + if (animate) { + + // Never allow two timeouts + clearTimeout(this.tooltipTimeout); + + // Set the fixed interval ticking for the smooth tooltip + this.tooltipTimeout = setTimeout(function () { + // The interval function may still be running during destroy, so check that the chart is really there before calling. + if (tooltip) { + tooltip.move(x, y, anchorX, anchorY); + } + }, 32); + + } + }, + + /** + * Hide the tooltip + */ + hide: function (delay) { + var tooltip = this; + clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766) + delay = pick(delay, this.options.hideDelay, 500); + if (!this.isHidden) { + this.hideTimer = syncTimeout(function () { + tooltip.label[delay ? 'fadeOut' : 'hide'](); + tooltip.isHidden = true; + }, delay); + } + }, + + /** + * Extendable method to get the anchor position of the tooltip + * from a point or set of points + */ + getAnchor: function (points, mouseEvent) { + var ret, + chart = this.chart, + inverted = chart.inverted, + plotTop = chart.plotTop, + plotLeft = chart.plotLeft, + plotX = 0, + plotY = 0, + yAxis, + xAxis; + + points = splat(points); + + // Pie uses a special tooltipPos + ret = points[0].tooltipPos; + + // When tooltip follows mouse, relate the position to the mouse + if (this.followPointer && mouseEvent) { + if (mouseEvent.chartX === UNDEFINED) { + mouseEvent = chart.pointer.normalize(mouseEvent); + } + ret = [ + mouseEvent.chartX - chart.plotLeft, + mouseEvent.chartY - plotTop + ]; + } + // When shared, use the average position + if (!ret) { + each(points, function (point) { + yAxis = point.series.yAxis; + xAxis = point.series.xAxis; + plotX += point.plotX + (!inverted && xAxis ? xAxis.left - plotLeft : 0); + plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) + + (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151 + }); + + plotX /= points.length; + plotY /= points.length; + + ret = [ + inverted ? chart.plotWidth - plotY : plotX, + this.shared && !inverted && points.length > 1 && mouseEvent ? + mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424) + inverted ? chart.plotHeight - plotX : plotY + ]; + } + + return map(ret, mathRound); + }, + + /** + * Place the tooltip in a chart without spilling over + * and not covering the point it self. + */ + getPosition: function (boxWidth, boxHeight, point) { + + var chart = this.chart, + distance = this.distance, + ret = {}, + h = point.h || 0, // #4117 + swapped, + first = ['y', chart.chartHeight, boxHeight, point.plotY + chart.plotTop, chart.plotTop, chart.plotTop + chart.plotHeight], + second = ['x', chart.chartWidth, boxWidth, point.plotX + chart.plotLeft, chart.plotLeft, chart.plotLeft + chart.plotWidth], + // The far side is right or bottom + preferFarSide = !this.followPointer && pick(point.ttBelow, !chart.inverted === !!point.negative), // #4984 + /** + * Handle the preferred dimension. When the preferred dimension is tooltip + * on top or bottom of the point, it will look for space there. + */ + firstDimension = function (dim, outerSize, innerSize, point, min, max) { + var roomLeft = innerSize < point - distance, + roomRight = point + distance + innerSize < outerSize, + alignedLeft = point - distance - innerSize, + alignedRight = point + distance; + + if (preferFarSide && roomRight) { + ret[dim] = alignedRight; + } else if (!preferFarSide && roomLeft) { + ret[dim] = alignedLeft; + } else if (roomLeft) { + ret[dim] = mathMin(max - innerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h); + } else if (roomRight) { + ret[dim] = mathMax(min, alignedRight + h + innerSize > outerSize ? alignedRight : alignedRight + h); + } else { + return false; + } + }, + /** + * Handle the secondary dimension. If the preferred dimension is tooltip + * on top or bottom of the point, the second dimension is to align the tooltip + * above the point, trying to align center but allowing left or right + * align within the chart box. + */ + secondDimension = function (dim, outerSize, innerSize, point) { + var retVal; + + // Too close to the edge, return false and swap dimensions + if (point < distance || point > outerSize - distance) { + retVal = false; + // Align left/top + } else if (point < innerSize / 2) { + ret[dim] = 1; + // Align right/bottom + } else if (point > outerSize - innerSize / 2) { + ret[dim] = outerSize - innerSize - 2; + // Align center + } else { + ret[dim] = point - innerSize / 2; + } + return retVal; + }, + /** + * Swap the dimensions + */ + swap = function (count) { + var temp = first; + first = second; + second = temp; + swapped = count; + }, + run = function () { + if (firstDimension.apply(0, first) !== false) { + if (secondDimension.apply(0, second) === false && !swapped) { + swap(true); + run(); + } + } else if (!swapped) { + swap(true); + run(); + } else { + ret.x = ret.y = 0; + } + }; + + // Under these conditions, prefer the tooltip on the side of the point + if (chart.inverted || this.len > 1) { + swap(); + } + run(); + + return ret; + + }, + + /** + * In case no user defined formatter is given, this will be used. Note that the context + * here is an object holding point, series, x, y etc. + */ + defaultFormatter: function (tooltip) { + var items = this.points || splat(this), + s; + + // build the header + s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; //#3397: abstraction to enable formatting of footer and header + + // build the values + s = s.concat(tooltip.bodyFormatter(items)); + + // footer + s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); //#3397: abstraction to enable formatting of footer and header + + return s.join(''); + }, + + /** + * Refresh the tooltip's text and position. + * @param {Object} point + */ + refresh: function (point, mouseEvent) { + var tooltip = this, + chart = tooltip.chart, + label = tooltip.label, + options = tooltip.options, + x, + y, + anchor, + textConfig = {}, + text, + pointConfig = [], + formatter = options.formatter || tooltip.defaultFormatter, + hoverPoints = chart.hoverPoints, + borderColor, + shared = tooltip.shared, + currentSeries; + + clearTimeout(this.hideTimer); + + // get the reference point coordinates (pie charts use tooltipPos) + tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer; + anchor = tooltip.getAnchor(point, mouseEvent); + x = anchor[0]; + y = anchor[1]; + + // shared tooltip, array is sent over + if (shared && !(point.series && point.series.noSharedTooltip)) { + + // hide previous hoverPoints and set new + + chart.hoverPoints = point; + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + each(point, function (item) { + item.setState(HOVER_STATE); + + pointConfig.push(item.getLabelConfig()); + }); + + textConfig = { + x: point[0].category, + y: point[0].y + }; + textConfig.points = pointConfig; + this.len = pointConfig.length; + point = point[0]; + + // single point tooltip + } else { + textConfig = point.getLabelConfig(); + } + text = formatter.call(textConfig, tooltip); + + // register the current series + currentSeries = point.series; + this.distance = pick(currentSeries.tooltipOptions.distance, 16); + + // update the inner HTML + if (text === false) { + this.hide(); + } else { + + // show it + if (tooltip.isHidden) { + stop(label); + label.attr('opacity', 1).show(); + } + + // update text + label.attr({ + text: text + }); + + // set the stroke color of the box + borderColor = options.borderColor || point.color || currentSeries.color || '#606060'; + label.attr({ + stroke: borderColor + }); + tooltip.updatePosition({ + plotX: x, + plotY: y, + negative: point.negative, + ttBelow: point.ttBelow, + h: anchor[2] || 0 + }); + + this.isHidden = false; + } + fireEvent(chart, 'tooltipRefresh', { + text: text, + x: x + chart.plotLeft, + y: y + chart.plotTop, + borderColor: borderColor + }); + }, + + /** + * Find the new position and perform the move + */ + updatePosition: function (point) { + var chart = this.chart, + label = this.label, + pos = (this.options.positioner || this.getPosition).call( + this, + label.width, + label.height, + point + ); + + // do the move + this.move( + mathRound(pos.x), + mathRound(pos.y || 0), // can be undefined (#3977) + point.plotX + chart.plotLeft, + point.plotY + chart.plotTop + ); + }, + + /** + * Get the best X date format based on the closest point range on the axis. + */ + getXDateFormat: function (point, options, xAxis) { + var xDateFormat, + dateTimeLabelFormats = options.dateTimeLabelFormats, + closestPointRange = xAxis && xAxis.closestPointRange, + n, + blank = '01-01 00:00:00.000', + strpos = { + millisecond: 15, + second: 12, + minute: 9, + hour: 6, + day: 3 + }, + date, + lastN = 'millisecond'; // for sub-millisecond data, #4223 + + if (closestPointRange) { + date = dateFormat('%m-%d %H:%M:%S.%L', point.x); + for (n in timeUnits) { + + // If the range is exactly one week and we're looking at a Sunday/Monday, go for the week format + if (closestPointRange === timeUnits.week && +dateFormat('%w', point.x) === xAxis.options.startOfWeek && + date.substr(6) === blank.substr(6)) { + n = 'week'; + break; + } + + // The first format that is too great for the range + if (timeUnits[n] > closestPointRange) { + n = lastN; + break; + } + + // If the point is placed every day at 23:59, we need to show + // the minutes as well. #2637. + if (strpos[n] && date.substr(strpos[n]) !== blank.substr(strpos[n])) { + break; + } + + // Weeks are outside the hierarchy, only apply them on Mondays/Sundays like in the first condition + if (n !== 'week') { + lastN = n; + } + } + + if (n) { + xDateFormat = dateTimeLabelFormats[n]; + } + } else { + xDateFormat = dateTimeLabelFormats.day; + } + + return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581 + }, + + /** + * Format the footer/header of the tooltip + * #3397: abstraction to enable formatting of footer and header + */ + tooltipFooterHeaderFormatter: function (point, isFooter) { + var footOrHead = isFooter ? 'footer' : 'header', + series = point.series, + tooltipOptions = series.tooltipOptions, + xDateFormat = tooltipOptions.xDateFormat, + xAxis = series.xAxis, + isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(point.key), + formatString = tooltipOptions[footOrHead + 'Format']; + + // Guess the best date format based on the closest point distance (#568, #3418) + if (isDateTime && !xDateFormat) { + xDateFormat = this.getXDateFormat(point, tooltipOptions, xAxis); + } + + // Insert the footer date format if any + if (isDateTime && xDateFormat) { + formatString = formatString.replace('{point.key}', '{point.key:' + xDateFormat + '}'); + } + + return format(formatString, { + point: point, + series: series + }); + }, + + /** + * Build the body (lines) of the tooltip by iterating over the items and returning one entry for each item, + * abstracting this functionality allows to easily overwrite and extend it. + */ + bodyFormatter: function (items) { + return map(items, function (item) { + var tooltipOptions = item.series.tooltipOptions; + return (tooltipOptions.pointFormatter || item.point.tooltipFormatter).call(item.point, tooltipOptions.pointFormat); + }); + } + + }; + + var hoverChartIndex; + + // Global flag for touch support + hasTouch = doc && doc.documentElement.ontouchstart !== UNDEFINED; + + /** + * The mouse tracker object. All methods starting with "on" are primary DOM event handlers. + * Subsequent methods should be named differently from what they are doing. + * @param {Object} chart The Chart instance + * @param {Object} options The root options object + */ + var Pointer = Highcharts.Pointer = function (chart, options) { + this.init(chart, options); + }; + + Pointer.prototype = { + /** + * Initialize Pointer + */ + init: function (chart, options) { + + var chartOptions = options.chart, + chartEvents = chartOptions.events, + zoomType = useCanVG ? '' : chartOptions.zoomType, + inverted = chart.inverted, + zoomX, + zoomY; + + // Store references + this.options = options; + this.chart = chart; + + // Zoom status + this.zoomX = zoomX = /x/.test(zoomType); + this.zoomY = zoomY = /y/.test(zoomType); + this.zoomHor = (zoomX && !inverted) || (zoomY && inverted); + this.zoomVert = (zoomY && !inverted) || (zoomX && inverted); + this.hasZoom = zoomX || zoomY; + + // Do we need to handle click on a touch device? + this.runChartClick = chartEvents && !!chartEvents.click; + + this.pinchDown = []; + this.lastValidTouch = {}; + + if (Highcharts.Tooltip && options.tooltip.enabled) { + chart.tooltip = new Tooltip(chart, options.tooltip); + this.followTouchMove = pick(options.tooltip.followTouchMove, true); + } + + this.setDOMEvents(); + }, + + /** + * Add crossbrowser support for chartX and chartY + * @param {Object} e The event object in standard browsers + */ + normalize: function (e, chartPosition) { + var chartX, + chartY, + ePos; + + // IE normalizing + e = e || win.event; + if (!e.target) { + e.target = e.srcElement; + } + + // iOS (#2757) + ePos = e.touches ? (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : e; + + // Get mouse position + if (!chartPosition) { + this.chartPosition = chartPosition = offset(this.chart.container); + } + + // chartX and chartY + if (ePos.pageX === UNDEFINED) { // IE < 9. #886. + chartX = mathMax(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is + // for IE10 quirks mode within framesets + chartY = e.y; + } else { + chartX = ePos.pageX - chartPosition.left; + chartY = ePos.pageY - chartPosition.top; + } + + return extend(e, { + chartX: mathRound(chartX), + chartY: mathRound(chartY) + }); + }, + + /** + * Get the click position in terms of axis values. + * + * @param {Object} e A pointer event + */ + getCoordinates: function (e) { + var coordinates = { + xAxis: [], + yAxis: [] + }; + + each(this.chart.axes, function (axis) { + coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({ + axis: axis, + value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY']) + }); + }); + return coordinates; + }, + + /** + * With line type charts with a single tracker, get the point closest to the mouse. + * Run Point.onMouseOver and display tooltip for the point or points. + */ + runPointActions: function (e) { + + var pointer = this, + chart = pointer.chart, + series = chart.series, + tooltip = chart.tooltip, + shared = tooltip ? tooltip.shared : false, + followPointer, + hoverPoint = chart.hoverPoint, + hoverSeries = chart.hoverSeries, + i, + distance = [Number.MAX_VALUE, Number.MAX_VALUE], // #4511 + anchor, + noSharedTooltip, + stickToHoverSeries, + directTouch, + kdpoints = [], + kdpoint = [], + kdpointT; + + // For hovering over the empty parts of the plot area (hoverSeries is undefined). + // If there is one series with point tracking (combo chart), don't go to nearest neighbour. + if (!shared && !hoverSeries) { + for (i = 0; i < series.length; i++) { + if (series[i].directTouch || !series[i].options.stickyTracking) { + series = []; + } + } + } + + // If it has a hoverPoint and that series requires direct touch (like columns, #3899), or we're on + // a noSharedTooltip series among shared tooltip series (#4546), use the hoverPoint . Otherwise, + // search the k-d tree. + stickToHoverSeries = hoverSeries && (shared ? hoverSeries.noSharedTooltip : hoverSeries.directTouch); + if (stickToHoverSeries && hoverPoint) { + kdpoint = [hoverPoint]; + + // Handle shared tooltip or cases where a series is not yet hovered + } else { + // Find nearest points on all series + each(series, function (s) { + // Skip hidden series + noSharedTooltip = s.noSharedTooltip && shared; + directTouch = !shared && s.directTouch; + if (s.visible && !noSharedTooltip && !directTouch && pick(s.options.enableMouseTracking, true)) { // #3821 + kdpointT = s.searchPoint(e, !noSharedTooltip && s.kdDimensions === 1); // #3828 + if (kdpointT && kdpointT.series) { // Point.series becomes null when reset and before redraw (#5197) + kdpoints.push(kdpointT); + } + } + }); + // Find absolute nearest point + each(kdpoints, function (p) { + if (p) { + // Store both closest points, using point.dist and point.distX comparisons (#4645): + each(['dist', 'distX'], function (dist, k) { + if (isNumber(p[dist])) { + var + // It is closer than the reference point + isCloser = p[dist] < distance[k], + // It is equally close, but above the reference point (#4679) + isAbove = p[dist] === distance[k] && p.series.group.zIndex >= kdpoint[k].series.group.zIndex; + + if (isCloser || isAbove) { + distance[k] = p[dist]; + kdpoint[k] = p; + } + } + }); + } + }); + } + + // Remove points with different x-positions, required for shared tooltip and crosshairs (#4645): + if (shared) { + i = kdpoints.length; + while (i--) { + if (kdpoints[i].clientX !== kdpoint[1].clientX || kdpoints[i].series.noSharedTooltip) { + kdpoints.splice(i, 1); + } + } + } + + // Refresh tooltip for kdpoint if new hover point or tooltip was hidden // #3926, #4200 + if (kdpoint[0] && (kdpoint[0] !== this.prevKDPoint || (tooltip && tooltip.isHidden))) { + // Draw tooltip if necessary + if (shared && !kdpoint[0].series.noSharedTooltip) { + if (kdpoints.length && tooltip) { + tooltip.refresh(kdpoints, e); + } + + // Do mouseover on all points (#3919, #3985, #4410) + each(kdpoints, function (point) { + point.onMouseOver(e, point !== ((hoverSeries && hoverSeries.directTouch && hoverPoint) || kdpoint[0])); + }); + this.prevKDPoint = kdpoint[1]; + } else { + if (tooltip) { + tooltip.refresh(kdpoint[0], e); + } + if (!hoverSeries || !hoverSeries.directTouch) { // #4448 + kdpoint[0].onMouseOver(e); + } + this.prevKDPoint = kdpoint[0]; + } + + // Update positions (regardless of kdpoint or hoverPoint) + } else { + followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer; + if (tooltip && followPointer && !tooltip.isHidden) { + anchor = tooltip.getAnchor([{}], e); + tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] }); + } + } + + // Start the event listener to pick up the tooltip and crosshairs + if (!pointer._onDocumentMouseMove) { + pointer._onDocumentMouseMove = function (e) { + if (charts[hoverChartIndex]) { + charts[hoverChartIndex].pointer.onDocumentMouseMove(e); + } + }; + addEvent(doc, 'mousemove', pointer._onDocumentMouseMove); + } + + // Crosshair. For each hover point, loop over axes and draw cross if that point + // belongs to the axis (#4927). + each(shared ? kdpoints : [pick(hoverPoint, kdpoint[1])], function (point) { // #5269 + each(chart.axes, function (axis) { + // In case of snap = false, point is undefined, and we draw the crosshair anyway (#5066) + if (!point || point.series[axis.coll] === axis) { + axis.drawCrosshair(e, point); + } + }); + }); + }, + + /** + * Reset the tracking by hiding the tooltip, the hover series state and the hover point + * + * @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible + */ + reset: function (allowMove, delay) { + var pointer = this, + chart = pointer.chart, + hoverSeries = chart.hoverSeries, + hoverPoint = chart.hoverPoint, + hoverPoints = chart.hoverPoints, + tooltip = chart.tooltip, + tooltipPoints = tooltip && tooltip.shared ? hoverPoints : hoverPoint; + + // Check if the points have moved outside the plot area (#1003, #4736, #5101) + if (allowMove && tooltipPoints) { + each(splat(tooltipPoints), function (point) { + if (point.series.isCartesian && point.plotX === undefined) { + allowMove = false; + } + }); + } + + // Just move the tooltip, #349 + if (allowMove) { + if (tooltip && tooltipPoints) { + tooltip.refresh(tooltipPoints); + if (hoverPoint) { // #2500 + hoverPoint.setState(hoverPoint.state, true); + each(chart.axes, function (axis) { + if (pick(axis.crosshair && axis.crosshair.snap, true)) { + axis.drawCrosshair(null, hoverPoint); + } else { + axis.hideCrosshair(); + } + }); + + } + } + + // Full reset + } else { + + if (hoverPoint) { + hoverPoint.onMouseOut(); + } + + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + if (hoverSeries) { + hoverSeries.onMouseOut(); + } + + if (tooltip) { + tooltip.hide(delay); + } + + if (pointer._onDocumentMouseMove) { + removeEvent(doc, 'mousemove', pointer._onDocumentMouseMove); + pointer._onDocumentMouseMove = null; + } + + // Remove crosshairs + each(chart.axes, function (axis) { + axis.hideCrosshair(); + }); + + pointer.hoverX = chart.hoverPoints = chart.hoverPoint = null; + + } + }, + + /** + * Scale series groups to a certain scale and translation + */ + scaleGroups: function (attribs, clip) { + + var chart = this.chart, + seriesAttribs; + + // Scale each series + each(chart.series, function (series) { + seriesAttribs = attribs || series.getPlotBox(); // #1701 + if (series.xAxis && series.xAxis.zoomEnabled) { + series.group.attr(seriesAttribs); + if (series.markerGroup) { + series.markerGroup.attr(seriesAttribs); + series.markerGroup.clip(clip ? chart.clipRect : null); + } + if (series.dataLabelsGroup) { + series.dataLabelsGroup.attr(seriesAttribs); + } + } + }); + + // Clip + chart.clipRect.attr(clip || chart.clipBox); + }, + + /** + * Start a drag operation + */ + dragStart: function (e) { + var chart = this.chart; + + // Record the start position + chart.mouseIsDown = e.type; + chart.cancelClick = false; + chart.mouseDownX = this.mouseDownX = e.chartX; + chart.mouseDownY = this.mouseDownY = e.chartY; + }, + + /** + * Perform a drag operation in response to a mousemove event while the mouse is down + */ + drag: function (e) { + + var chart = this.chart, + chartOptions = chart.options.chart, + chartX = e.chartX, + chartY = e.chartY, + zoomHor = this.zoomHor, + zoomVert = this.zoomVert, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + clickedInside, + size, + selectionMarker = this.selectionMarker, + mouseDownX = this.mouseDownX, + mouseDownY = this.mouseDownY, + panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key']; + + // If the device supports both touch and mouse (like IE11), and we are touch-dragging + // inside the plot area, don't handle the mouse event. #4339. + if (selectionMarker && selectionMarker.touch) { + return; + } + + // If the mouse is outside the plot area, adjust to cooordinates + // inside to prevent the selection marker from going outside + 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; + } + + // determine if the mouse has moved more than 10px + this.hasDragged = Math.sqrt( + Math.pow(mouseDownX - chartX, 2) + + Math.pow(mouseDownY - chartY, 2) + ); + + if (this.hasDragged > 10) { + clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop); + + // make a selection + if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside && !panKey) { + if (!selectionMarker) { + this.selectionMarker = selectionMarker = chart.renderer.rect( + plotLeft, + plotTop, + zoomHor ? 1 : plotWidth, + zoomVert ? 1 : plotHeight, + 0 + ) + .attr({ + fill: chartOptions.selectionMarkerFill || 'rgba(69,114,167,0.25)', + zIndex: 7 + }) + .add(); + } + } + + // adjust the width of the selection marker + if (selectionMarker && zoomHor) { + size = chartX - mouseDownX; + selectionMarker.attr({ + width: mathAbs(size), + x: (size > 0 ? 0 : size) + mouseDownX + }); + } + // adjust the height of the selection marker + if (selectionMarker && zoomVert) { + size = chartY - mouseDownY; + selectionMarker.attr({ + height: mathAbs(size), + y: (size > 0 ? 0 : size) + mouseDownY + }); + } + + // panning + if (clickedInside && !selectionMarker && chartOptions.panning) { + chart.pan(e, chartOptions.panning); + } + } + }, + + /** + * On mouse up or touch end across the entire document, drop the selection. + */ + drop: function (e) { + var pointer = this, + chart = this.chart, + hasPinched = this.hasPinched; + + if (this.selectionMarker) { + var selectionData = { + originalEvent: e, // #4890 + xAxis: [], + yAxis: [] + }, + selectionBox = this.selectionMarker, + selectionLeft = selectionBox.attr ? selectionBox.attr('x') : selectionBox.x, + selectionTop = selectionBox.attr ? selectionBox.attr('y') : selectionBox.y, + selectionWidth = selectionBox.attr ? selectionBox.attr('width') : selectionBox.width, + selectionHeight = selectionBox.attr ? selectionBox.attr('height') : selectionBox.height, + runZoom; + + // a selection has been made + if (this.hasDragged || hasPinched) { + + // record each axis' min and max + each(chart.axes, function (axis) { + if (axis.zoomEnabled && defined(axis.min) && (hasPinched || pointer[{ xAxis: 'zoomX', yAxis: 'zoomY' }[axis.coll]])) { // #859, #3569 + var horiz = axis.horiz, + minPixelPadding = e.type === 'touchend' ? axis.minPixelPadding : 0, // #1207, #3075 + selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop) + minPixelPadding), + selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight) - minPixelPadding); + + selectionData[axis.coll].push({ + axis: axis, + min: mathMin(selectionMin, selectionMax), // for reversed axes + max: mathMax(selectionMin, selectionMax) + }); + runZoom = true; + } + }); + if (runZoom) { + fireEvent(chart, 'selection', selectionData, function (args) { + chart.zoom(extend(args, hasPinched ? { animation: false } : null)); + }); + } + + } + this.selectionMarker = this.selectionMarker.destroy(); + + // Reset scaling preview + if (hasPinched) { + this.scaleGroups(); + } + } + + // Reset all + if (chart) { // it may be destroyed on mouse up - #877 + css(chart.container, { cursor: chart._cursor }); + chart.cancelClick = this.hasDragged > 10; // #370 + chart.mouseIsDown = this.hasDragged = this.hasPinched = false; + this.pinchDown = []; + } + }, + + onContainerMouseDown: function (e) { + + e = this.normalize(e); + + // issue #295, dragging not always working in Firefox + if (e.preventDefault) { + e.preventDefault(); + } + + this.dragStart(e); + }, + + + + onDocumentMouseUp: function (e) { + if (charts[hoverChartIndex]) { + charts[hoverChartIndex].pointer.drop(e); + } + }, + + /** + * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea. + * Issue #149 workaround. The mouseleave event does not always fire. + */ + onDocumentMouseMove: function (e) { + var chart = this.chart, + chartPosition = this.chartPosition; + + e = this.normalize(e, chartPosition); + + // If we're outside, hide the tooltip + if (chartPosition && !this.inClass(e.target, 'highcharts-tracker') && + !chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { + this.reset(); + } + }, + + /** + * When mouse leaves the container, hide the tooltip. + */ + onContainerMouseLeave: function (e) { + var chart = charts[hoverChartIndex]; + if (chart && (e.relatedTarget || e.toElement)) { // #4886, MS Touch end fires mouseleave but with no related target + chart.pointer.reset(); + chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix + } + }, + + // The mousemove, touchmove and touchstart event handler + onContainerMouseMove: function (e) { + + var chart = this.chart; + + if (!defined(hoverChartIndex) || !charts[hoverChartIndex] || !charts[hoverChartIndex].mouseIsDown) { + hoverChartIndex = chart.index; + } + + e = this.normalize(e); + e.returnValue = false; // #2251, #3224 + + if (chart.mouseIsDown === 'mousedown') { + this.drag(e); + } + + // Show the tooltip and run mouse over events (#977) + if ((this.inClass(e.target, 'highcharts-tracker') || + chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) { + this.runPointActions(e); + } + }, + + /** + * Utility to detect whether an element has, or has a parent with, a specific + * class name. Used on detection of tracker objects and on deciding whether + * hovering the tooltip should cause the active series to mouse out. + */ + inClass: function (element, className) { + var elemClassName; + while (element) { + elemClassName = attr(element, 'class'); + if (elemClassName) { + if (elemClassName.indexOf(className) !== -1) { + return true; + } + if (elemClassName.indexOf(PREFIX + 'container') !== -1) { + return false; + } + } + element = element.parentNode; + } + }, + + onTrackerMouseOut: function (e) { + var series = this.chart.hoverSeries, + relatedTarget = e.relatedTarget || e.toElement; + + if (series && relatedTarget && !series.options.stickyTracking && // #4886 + !this.inClass(relatedTarget, PREFIX + 'tooltip') && + !this.inClass(relatedTarget, PREFIX + 'series-' + series.index)) { // #2499, #4465 + series.onMouseOut(); + } + }, + + onContainerClick: function (e) { + var chart = this.chart, + hoverPoint = chart.hoverPoint, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop; + + e = this.normalize(e); + + if (!chart.cancelClick) { + + // On tracker click, fire the series and point events. #783, #1583 + if (hoverPoint && this.inClass(e.target, PREFIX + 'tracker')) { + + // the series click event + fireEvent(hoverPoint.series, 'click', extend(e, { + point: hoverPoint + })); + + // the point click event + if (chart.hoverPoint) { // it may be destroyed (#1844) + hoverPoint.firePointEvent('click', e); + } + + // When clicking outside a tracker, fire a chart event + } else { + extend(e, this.getCoordinates(e)); + + // fire a click event in the chart + if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) { + fireEvent(chart, 'click', e); + } + } + + + } + }, + + /** + * Set the JS DOM events on the container and document. This method should contain + * a one-to-one assignment between methods and their handlers. Any advanced logic should + * be moved to the handler reflecting the event's name. + */ + setDOMEvents: function () { + + var pointer = this, + container = pointer.chart.container; + + container.onmousedown = function (e) { + pointer.onContainerMouseDown(e); + }; + container.onmousemove = function (e) { + pointer.onContainerMouseMove(e); + }; + container.onclick = function (e) { + pointer.onContainerClick(e); + }; + addEvent(container, 'mouseleave', pointer.onContainerMouseLeave); + if (chartCount === 1) { + addEvent(doc, 'mouseup', pointer.onDocumentMouseUp); + } + if (hasTouch) { + container.ontouchstart = function (e) { + pointer.onContainerTouchStart(e); + }; + container.ontouchmove = function (e) { + pointer.onContainerTouchMove(e); + }; + if (chartCount === 1) { + addEvent(doc, 'touchend', pointer.onDocumentTouchEnd); + } + } + + }, + + /** + * Destroys the Pointer object and disconnects DOM events. + */ + destroy: function () { + var prop; + + removeEvent(this.chart.container, 'mouseleave', this.onContainerMouseLeave); + if (!chartCount) { + removeEvent(doc, 'mouseup', this.onDocumentMouseUp); + removeEvent(doc, 'touchend', this.onDocumentTouchEnd); + } + + // memory and CPU leak + clearInterval(this.tooltipTimeout); + + for (prop in this) { + this[prop] = null; + } + } + }; + + + /* Support for touch devices */ + extend(Highcharts.Pointer.prototype, { + + /** + * Run translation operations + */ + pinchTranslate: function (pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) { + if (this.zoomHor || this.pinchHor) { + this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); + } + if (this.zoomVert || this.pinchVert) { + this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); + } + }, + + /** + * Run translation operations for each direction (horizontal and vertical) independently + */ + pinchTranslateDirection: function (horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch, forcedScale) { + var chart = this.chart, + xy = horiz ? 'x' : 'y', + XY = horiz ? 'X' : 'Y', + sChartXY = 'chart' + XY, + wh = horiz ? 'width' : 'height', + plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')], + selectionWH, + selectionXY, + clipXY, + scale = forcedScale || 1, + inverted = chart.inverted, + bounds = chart.bounds[horiz ? 'h' : 'v'], + singleTouch = pinchDown.length === 1, + touch0Start = pinchDown[0][sChartXY], + touch0Now = touches[0][sChartXY], + touch1Start = !singleTouch && pinchDown[1][sChartXY], + touch1Now = !singleTouch && touches[1][sChartXY], + outOfBounds, + transformScale, + scaleKey, + setScale = function () { + if (!singleTouch && mathAbs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis + scale = forcedScale || mathAbs(touch0Now - touch1Now) / mathAbs(touch0Start - touch1Start); + } + + clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start; + selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale; + }; + + // Set the scale, first pass + setScale(); + + selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not + + // Out of bounds + if (selectionXY < bounds.min) { + selectionXY = bounds.min; + outOfBounds = true; + } else if (selectionXY + selectionWH > bounds.max) { + selectionXY = bounds.max - selectionWH; + outOfBounds = true; + } + + // Is the chart dragged off its bounds, determined by dataMin and dataMax? + if (outOfBounds) { + + // Modify the touchNow position in order to create an elastic drag movement. This indicates + // to the user that the chart is responsive but can't be dragged further. + touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]); + if (!singleTouch) { + touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]); + } + + // Set the scale, second pass to adapt to the modified touchNow positions + setScale(); + + } else { + lastValidTouch[xy] = [touch0Now, touch1Now]; + } + + // Set geometry for clipping, selection and transformation + if (!inverted) { + clip[xy] = clipXY - plotLeftTop; + clip[wh] = selectionWH; + } + scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY; + transformScale = inverted ? 1 / scale : scale; + + selectionMarker[wh] = selectionWH; + selectionMarker[xy] = selectionXY; + transform[scaleKey] = scale; + transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start)); + }, + + /** + * Handle touch events with two touches + */ + pinch: function (e) { + + var self = this, + chart = self.chart, + pinchDown = self.pinchDown, + touches = e.touches, + touchesLength = touches.length, + lastValidTouch = self.lastValidTouch, + hasZoom = self.hasZoom, + selectionMarker = self.selectionMarker, + transform = {}, + fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, PREFIX + 'tracker') && + chart.runTrackerClick) || self.runChartClick), + clip = {}; + + // Don't initiate panning until the user has pinched. This prevents us from + // blocking page scrolling as users scroll down a long page (#4210). + if (touchesLength > 1) { + self.initiated = true; + } + + // On touch devices, only proceed to trigger click if a handler is defined + if (hasZoom && self.initiated && !fireClickEvent) { + e.preventDefault(); + } + + // Normalize each touch + map(touches, function (e) { + return self.normalize(e); + }); + + // Register the touch start position + if (e.type === 'touchstart') { + each(touches, function (e, i) { + pinchDown[i] = { chartX: e.chartX, chartY: e.chartY }; + }); + lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX]; + lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY]; + + // Identify the data bounds in pixels + each(chart.axes, function (axis) { + if (axis.zoomEnabled) { + var bounds = chart.bounds[axis.horiz ? 'h' : 'v'], + minPixelPadding = axis.minPixelPadding, + min = axis.toPixels(pick(axis.options.min, axis.dataMin)), + max = axis.toPixels(pick(axis.options.max, axis.dataMax)), + absMin = mathMin(min, max), + absMax = mathMax(min, max); + + // Store the bounds for use in the touchmove handler + bounds.min = mathMin(axis.pos, absMin - minPixelPadding); + bounds.max = mathMax(axis.pos + axis.len, absMax + minPixelPadding); + } + }); + self.res = true; // reset on next move + + // Event type is touchmove, handle panning and pinching + } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first + + + // Set the marker + if (!selectionMarker) { + self.selectionMarker = selectionMarker = extend({ + destroy: noop, + touch: true + }, chart.plotBox); + } + + self.pinchTranslate(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); + + self.hasPinched = hasZoom; + + // Scale and translate the groups to provide visual feedback during pinching + self.scaleGroups(transform, clip); + + // Optionally move the tooltip on touchmove + if (!hasZoom && self.followTouchMove && touchesLength === 1) { + this.runPointActions(self.normalize(e)); + } else if (self.res) { + self.res = false; + this.reset(false, 0); + } + } + }, + + /** + * General touch handler shared by touchstart and touchmove. + */ + touch: function (e, start) { + var chart = this.chart, + hasMoved, + pinchDown; + + hoverChartIndex = chart.index; + + if (e.touches.length === 1) { + + e = this.normalize(e); + + if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop) && !chart.openMenu) { + + // Run mouse events and display tooltip etc + if (start) { + this.runPointActions(e); + } + + // Android fires touchmove events after the touchstart even if the + // finger hasn't moved, or moved only a pixel or two. In iOS however, + // the touchmove doesn't fire unless the finger moves more than ~4px. + // So we emulate this behaviour in Android by checking how much it + // moved, and cancelling on small distances. #3450. + if (e.type === 'touchmove') { + pinchDown = this.pinchDown; + hasMoved = pinchDown[0] ? Math.sqrt( // #5266 + Math.pow(pinchDown[0].chartX - e.chartX, 2) + + Math.pow(pinchDown[0].chartY - e.chartY, 2) + ) >= 4 : false; + } + + if (pick(hasMoved, true)) { + this.pinch(e); + } + + } else if (start) { + // Hide the tooltip on touching outside the plot area (#1203) + this.reset(); + } + + } else if (e.touches.length === 2) { + this.pinch(e); + } + }, + + onContainerTouchStart: function (e) { + this.touch(e, true); + }, + + onContainerTouchMove: function (e) { + this.touch(e); + }, + + onDocumentTouchEnd: function (e) { + if (charts[hoverChartIndex]) { + charts[hoverChartIndex].pointer.drop(e); + } + } + + }); + if (win.PointerEvent || win.MSPointerEvent) { + + // The touches object keeps track of the points being touched at all times + var touches = {}, + hasPointerEvent = !!win.PointerEvent, + getWebkitTouches = function () { + var key, + fake = []; + fake.item = function (i) { + return this[i]; + }; + for (key in touches) { + if (touches.hasOwnProperty(key)) { + fake.push({ + pageX: touches[key].pageX, + pageY: touches[key].pageY, + target: touches[key].target + }); + } + } + return fake; + }, + translateMSPointer = function (e, method, wktype, func) { + var p; + if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[hoverChartIndex]) { + func(e); + p = charts[hoverChartIndex].pointer; + p[method]({ + type: wktype, + target: e.currentTarget, + preventDefault: noop, + touches: getWebkitTouches() + }); + } + }; + + /** + * Extend the Pointer prototype with methods for each event handler and more + */ + extend(Pointer.prototype, { + onContainerPointerDown: function (e) { + translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function (e) { + touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY, target: e.currentTarget }; + }); + }, + onContainerPointerMove: function (e) { + translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function (e) { + touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY }; + if (!touches[e.pointerId].target) { + touches[e.pointerId].target = e.currentTarget; + } + }); + }, + onDocumentPointerUp: function (e) { + translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function (e) { + delete touches[e.pointerId]; + }); + }, + + /** + * Add or remove the MS Pointer specific events + */ + batchMSEvents: function (fn) { + fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown); + fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove); + fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp); + } + }); + + // Disable default IE actions for pinch and such on chart element + wrap(Pointer.prototype, 'init', function (proceed, chart, options) { + proceed.call(this, chart, options); + if (this.hasZoom) { // #4014 + css(chart.container, { + '-ms-touch-action': NONE, + 'touch-action': NONE + }); + } + }); + + // Add IE specific touch events to chart + wrap(Pointer.prototype, 'setDOMEvents', function (proceed) { + proceed.apply(this); + if (this.hasZoom || this.followTouchMove) { + this.batchMSEvents(addEvent); + } + }); + // Destroy MS events also + wrap(Pointer.prototype, 'destroy', function (proceed) { + this.batchMSEvents(removeEvent); + proceed.call(this); + }); + } + /** + * The overview of the chart's series + */ + var Legend = Highcharts.Legend = function (chart, options) { + this.init(chart, options); + }; + + Legend.prototype = { + + /** + * Initialize the legend + */ + init: function (chart, options) { + + var legend = this, + itemStyle = options.itemStyle, + padding, + itemMarginTop = options.itemMarginTop || 0; + + this.options = options; + + if (!options.enabled) { + return; + } + + legend.itemStyle = itemStyle; + legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle); + legend.itemMarginTop = itemMarginTop; + legend.padding = padding = pick(options.padding, 8); + legend.initialItemX = padding; + legend.initialItemY = padding - 5; // 5 is the number of pixels above the text + legend.maxItemWidth = 0; + legend.chart = chart; + legend.itemHeight = 0; + legend.symbolWidth = pick(options.symbolWidth, 16); + legend.pages = []; + + + // Render it + legend.render(); + + // move checkboxes + addEvent(legend.chart, 'endResize', function () { + legend.positionCheckboxes(); + }); + + }, + + /** + * Set the colors for the legend item + * @param {Object} item A Series or Point instance + * @param {Object} visible Dimmed or colored + */ + colorizeItem: function (item, visible) { + var legend = this, + options = legend.options, + legendItem = item.legendItem, + legendLine = item.legendLine, + legendSymbol = item.legendSymbol, + hiddenColor = legend.itemHiddenStyle.color, + textColor = visible ? options.itemStyle.color : hiddenColor, + symbolColor = visible ? (item.legendColor || item.color || '#CCC') : hiddenColor, + markerOptions = item.options && item.options.marker, + symbolAttr = { fill: symbolColor }, + key, + val; + + if (legendItem) { + legendItem.css({ fill: textColor, color: textColor }); // color for #1553, oldIE + } + if (legendLine) { + legendLine.attr({ stroke: symbolColor }); + } + + if (legendSymbol) { + + // Apply marker options + if (markerOptions && legendSymbol.isMarker) { // #585 + symbolAttr.stroke = symbolColor; + markerOptions = item.convertAttribs(markerOptions); + for (key in markerOptions) { + val = markerOptions[key]; + if (val !== UNDEFINED) { + symbolAttr[key] = val; + } + } + } + + legendSymbol.attr(symbolAttr); + } + }, + + /** + * Position the legend item + * @param {Object} item A Series or Point instance + */ + positionItem: function (item) { + var legend = this, + options = legend.options, + symbolPadding = options.symbolPadding, + ltr = !options.rtl, + legendItemPos = item._legendItemPos, + itemX = legendItemPos[0], + itemY = legendItemPos[1], + checkbox = item.checkbox, + legendGroup = item.legendGroup; + + if (legendGroup && legendGroup.element) { + legendGroup.translate( + ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4, + itemY + ); + } + + if (checkbox) { + checkbox.x = itemX; + checkbox.y = itemY; + } + }, + + /** + * Destroy a single legend item + * @param {Object} item The series or point + */ + destroyItem: function (item) { + var checkbox = item.checkbox; + + // destroy SVG elements + each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function (key) { + if (item[key]) { + item[key] = item[key].destroy(); + } + }); + + if (checkbox) { + discardElement(item.checkbox); + } + }, + + /** + * Destroys the legend. + */ + destroy: function () { + var legend = this, + legendGroup = legend.group, + box = legend.box; + + if (box) { + legend.box = box.destroy(); + } + + if (legendGroup) { + legend.group = legendGroup.destroy(); + } + }, + + /** + * Position the checkboxes after the width is determined + */ + positionCheckboxes: function (scrollOffset) { + var alignAttr = this.group.alignAttr, + translateY, + clipHeight = this.clipHeight || this.legendHeight, + titleHeight = this.titleHeight; + + if (alignAttr) { + translateY = alignAttr.translateY; + each(this.allItems, function (item) { + var checkbox = item.checkbox, + top; + + if (checkbox) { + top = translateY + titleHeight + checkbox.y + (scrollOffset || 0) + 3; + css(checkbox, { + left: (alignAttr.translateX + item.checkboxOffset + checkbox.x - 20) + PX, + top: top + PX, + display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : NONE + }); + } + }); + } + }, + + /** + * Render the legend title on top of the legend + */ + renderTitle: function () { + var options = this.options, + padding = this.padding, + titleOptions = options.title, + titleHeight = 0, + bBox; + + if (titleOptions.text) { + if (!this.title) { + this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title') + .attr({ zIndex: 1 }) + .css(titleOptions.style) + .add(this.group); + } + bBox = this.title.getBBox(); + titleHeight = bBox.height; + this.offsetWidth = bBox.width; // #1717 + this.contentGroup.attr({ translateY: titleHeight }); + } + this.titleHeight = titleHeight; + }, + + /** + * Set the legend item text + */ + setText: function (item) { + var options = this.options; + item.legendItem.attr({ + text: options.labelFormat ? format(options.labelFormat, item) : options.labelFormatter.call(item) + }); + }, + + /** + * Render a single specific legend item + * @param {Object} item A series or point + */ + renderItem: function (item) { + var legend = this, + chart = legend.chart, + renderer = chart.renderer, + options = legend.options, + horizontal = options.layout === 'horizontal', + symbolWidth = legend.symbolWidth, + symbolPadding = options.symbolPadding, + itemStyle = legend.itemStyle, + itemHiddenStyle = legend.itemHiddenStyle, + padding = legend.padding, + itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, + ltr = !options.rtl, + itemHeight, + widthOption = options.width, + itemMarginBottom = options.itemMarginBottom || 0, + itemMarginTop = legend.itemMarginTop, + initialItemX = legend.initialItemX, + bBox, + itemWidth, + li = item.legendItem, + series = item.series && item.series.drawLegendSymbol ? item.series : item, + seriesOptions = series.options, + showCheckbox = legend.createCheckboxForItem && seriesOptions && seriesOptions.showCheckbox, + useHTML = options.useHTML; + + if (!li) { // generate it once, later move it + + // Generate the group box + // A group to hold the symbol and text. Text is to be appended in Legend class. + item.legendGroup = renderer.g('legend-item') + .attr({ zIndex: 1 }) + .add(legend.scrollGroup); + + // Generate the list item text and add it to the group + item.legendItem = li = renderer.text( + '', + ltr ? symbolWidth + symbolPadding : -symbolPadding, + legend.baseline || 0, + useHTML + ) + .css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021) + .attr({ + align: ltr ? 'left' : 'right', + zIndex: 2 + }) + .add(item.legendGroup); + + // Get the baseline for the first item - the font size is equal for all + if (!legend.baseline) { + legend.fontMetrics = renderer.fontMetrics(itemStyle.fontSize, li); + legend.baseline = legend.fontMetrics.f + 3 + itemMarginTop; + li.attr('y', legend.baseline); + } + + // Draw the legend symbol inside the group box + series.drawLegendSymbol(legend, item); + + if (legend.setItemEvents) { + legend.setItemEvents(item, li, useHTML, itemStyle, itemHiddenStyle); + } + + // add the HTML checkbox on top + if (showCheckbox) { + legend.createCheckboxForItem(item); + } + } + + // Colorize the items + legend.colorizeItem(item, item.visible); + + // Always update the text + legend.setText(item); + + // calculate the positions for the next line + bBox = li.getBBox(); + + itemWidth = item.checkboxOffset = + options.itemWidth || + item.legendItemWidth || + symbolWidth + symbolPadding + bBox.width + itemDistance + (showCheckbox ? 20 : 0); + legend.itemHeight = itemHeight = mathRound(item.legendItemHeight || bBox.height); + + // if the item exceeds the width, start a new line + if (horizontal && legend.itemX - initialItemX + itemWidth > + (widthOption || (chart.chartWidth - 2 * padding - initialItemX - options.x))) { + legend.itemX = initialItemX; + legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom; + legend.lastLineHeight = 0; // reset for next line (#915, #3976) + } + + // If the item exceeds the height, start a new column + /*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) { + legend.itemY = legend.initialItemY; + legend.itemX += legend.maxItemWidth; + legend.maxItemWidth = 0; + }*/ + + // Set the edge positions + legend.maxItemWidth = mathMax(legend.maxItemWidth, itemWidth); + legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom; + legend.lastLineHeight = mathMax(itemHeight, legend.lastLineHeight); // #915 + + // cache the position of the newly generated or reordered items + item._legendItemPos = [legend.itemX, legend.itemY]; + + // advance + if (horizontal) { + legend.itemX += itemWidth; + + } else { + legend.itemY += itemMarginTop + itemHeight + itemMarginBottom; + legend.lastLineHeight = itemHeight; + } + + // the width of the widest item + legend.offsetWidth = widthOption || mathMax( + (horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding, + legend.offsetWidth + ); + }, + + /** + * Get all items, which is one item per series for normal series and one item per point + * for pie series. + */ + getAllItems: function () { + var allItems = []; + each(this.chart.series, function (series) { + var seriesOptions = series.options; + + // Handle showInLegend. If the series is linked to another series, defaults to false. + if (!pick(seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? UNDEFINED : false, true)) { + return; + } + + // use points or series for the legend item depending on legendType + allItems = allItems.concat( + series.legendItems || + (seriesOptions.legendType === 'point' ? + series.data : + series) + ); + }); + return allItems; + }, + + /** + * Adjust the chart margins by reserving space for the legend on only one side + * of the chart. If the position is set to a corner, top or bottom is reserved + * for horizontal legends and left or right for vertical ones. + */ + adjustMargins: function (margin, spacing) { + var chart = this.chart, + options = this.options, + // Use the first letter of each alignment option in order to detect the side + alignment = options.align.charAt(0) + options.verticalAlign.charAt(0) + options.layout.charAt(0); // #4189 - use charAt(x) notation instead of [x] for IE7 + + if (!options.floating) { + + each([ + /(lth|ct|rth)/, + /(rtv|rm|rbv)/, + /(rbh|cb|lbh)/, + /(lbv|lm|ltv)/ + ], function (alignments, side) { + if (alignments.test(alignment) && !defined(margin[side])) { + // Now we have detected on which side of the chart we should reserve space for the legend + chart[marginNames[side]] = mathMax( + chart[marginNames[side]], + chart.legend[(side + 1) % 2 ? 'legendHeight' : 'legendWidth'] + + [1, -1, -1, 1][side] * options[(side % 2) ? 'x' : 'y'] + + pick(options.margin, 12) + + spacing[side] + ); + } + }); + } + }, + + /** + * 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. + */ + render: function () { + var legend = this, + chart = legend.chart, + renderer = chart.renderer, + legendGroup = legend.group, + allItems, + display, + legendWidth, + legendHeight, + box = legend.box, + options = legend.options, + padding = legend.padding, + legendBorderWidth = options.borderWidth, + legendBackgroundColor = options.backgroundColor; + + legend.itemX = legend.initialItemX; + legend.itemY = legend.initialItemY; + legend.offsetWidth = 0; + legend.lastItemY = 0; + + if (!legendGroup) { + legend.group = legendGroup = renderer.g('legend') + .attr({ zIndex: 7 }) + .add(); + legend.contentGroup = renderer.g() + .attr({ zIndex: 1 }) // above background + .add(legendGroup); + legend.scrollGroup = renderer.g() + .add(legend.contentGroup); + } + + legend.renderTitle(); + + // add each series or point + allItems = legend.getAllItems(); + + // sort by legendIndex + stableSort(allItems, function (a, b) { + return ((a.options && a.options.legendIndex) || 0) - ((b.options && b.options.legendIndex) || 0); + }); + + // reversed legend + if (options.reversed) { + allItems.reverse(); + } + + legend.allItems = allItems; + legend.display = display = !!allItems.length; + + // render the items + legend.lastLineHeight = 0; + each(allItems, function (item) { + legend.renderItem(item); + }); + + // Get the box + legendWidth = (options.width || legend.offsetWidth) + padding; + legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight; + legendHeight = legend.handleOverflow(legendHeight); + legendHeight += padding; + + // Draw the border and/or background + if (legendBorderWidth || legendBackgroundColor) { + + if (!box) { + legend.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({ width: legendWidth, height: legendHeight }) + ); + box.isNew = false; + } + + // hide the border if no items + box[display ? 'show' : 'hide'](); + } + + legend.legendWidth = legendWidth; + legend.legendHeight = legendHeight; + + // Now that the legend width and height are established, put the items in the + // final position + each(allItems, function (item) { + legend.positionItem(item); + }); + + // 1.x compatibility: positioning based on style + /*var props = ['left', 'right', 'top', 'bottom'], + prop, + i = 4; + while (i--) { + prop = props[i]; + if (options.style[prop] && options.style[prop] !== 'auto') { + options[i < 2 ? 'align' : 'verticalAlign'] = prop; + options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1); + } + }*/ + + if (display) { + legendGroup.align(extend({ + width: legendWidth, + height: legendHeight + }, options), true, 'spacingBox'); + } + + if (!chart.isResizing) { + this.positionCheckboxes(); + } + }, + + /** + * Set up the overflow handling by adding navigation with up and down arrows below the + * legend. + */ + handleOverflow: function (legendHeight) { + var legend = this, + chart = this.chart, + renderer = chart.renderer, + options = this.options, + optionsY = options.y, + alignTop = options.verticalAlign === 'top', + spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding, + maxHeight = options.maxHeight, + clipHeight, + clipRect = this.clipRect, + navOptions = options.navigation, + animation = pick(navOptions.animation, true), + arrowSize = navOptions.arrowSize || 12, + nav = this.nav, + pages = this.pages, + padding = this.padding, + lastY, + allItems = this.allItems, + clipToHeight = function (height) { + clipRect.attr({ + height: height + }); + + // useHTML + if (legend.contentGroup.div) { + legend.contentGroup.div.style.clip = 'rect(' + padding + 'px,9999px,' + (padding + height) + 'px,0)'; + } + }; + + + // Adjust the height + if (options.layout === 'horizontal') { + spaceHeight /= 2; + } + if (maxHeight) { + spaceHeight = mathMin(spaceHeight, maxHeight); + } + + // Reset the legend height and adjust the clipping rectangle + pages.length = 0; + if (legendHeight > spaceHeight && navOptions.enabled !== false) { + + this.clipHeight = clipHeight = mathMax(spaceHeight - 20 - this.titleHeight - padding, 0); + this.currentPage = pick(this.currentPage, 1); + this.fullHeight = legendHeight; + + // Fill pages with Y positions so that the top of each a legend item defines + // the scroll top for each page (#2098) + each(allItems, function (item, i) { + var y = item._legendItemPos[1], + h = mathRound(item.legendItem.getBBox().height), + len = pages.length; + + if (!len || (y - pages[len - 1] > clipHeight && (lastY || y) !== pages[len - 1])) { + pages.push(lastY || y); + len++; + } + + if (i === allItems.length - 1 && y + h - pages[len - 1] > clipHeight) { + pages.push(y); + } + if (y !== lastY) { + lastY = y; + } + }); + + // Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787) + if (!clipRect) { + clipRect = legend.clipRect = renderer.clipRect(0, padding, 9999, 0); + legend.contentGroup.clip(clipRect); + } + + clipToHeight(clipHeight); + + // Add navigation elements + if (!nav) { + this.nav = nav = renderer.g().attr({ zIndex: 1 }).add(this.group); + this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize) + .on('click', function () { + legend.scroll(-1, animation); + }) + .add(nav); + this.pager = renderer.text('', 15, 10) + .css(navOptions.style) + .add(nav); + this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize) + .on('click', function () { + legend.scroll(1, animation); + }) + .add(nav); + } + + // Set initial position + legend.scroll(0); + + legendHeight = spaceHeight; + + } else if (nav) { + clipToHeight(chart.chartHeight); + nav.hide(); + this.scrollGroup.attr({ + translateY: 1 + }); + this.clipHeight = 0; // #1379 + } + + return legendHeight; + }, + + /** + * Scroll the legend by a number of pages + * @param {Object} scrollBy + * @param {Object} animation + */ + scroll: function (scrollBy, animation) { + var pages = this.pages, + pageCount = pages.length, + currentPage = this.currentPage + scrollBy, + clipHeight = this.clipHeight, + navOptions = this.options.navigation, + activeColor = navOptions.activeColor, + inactiveColor = navOptions.inactiveColor, + pager = this.pager, + padding = this.padding, + scrollOffset; + + // When resizing while looking at the last page + if (currentPage > pageCount) { + currentPage = pageCount; + } + + if (currentPage > 0) { + + if (animation !== UNDEFINED) { + setAnimation(animation, this.chart); + } + + this.nav.attr({ + translateX: padding, + translateY: clipHeight + this.padding + 7 + this.titleHeight, + visibility: VISIBLE + }); + this.up.attr({ + fill: currentPage === 1 ? inactiveColor : activeColor + }) + .css({ + cursor: currentPage === 1 ? 'default' : 'pointer' + }); + pager.attr({ + text: currentPage + '/' + pageCount + }); + this.down.attr({ + x: 18 + this.pager.getBBox().width, // adjust to text width + fill: currentPage === pageCount ? inactiveColor : activeColor + }) + .css({ + cursor: currentPage === pageCount ? 'default' : 'pointer' + }); + + scrollOffset = -pages[currentPage - 1] + this.initialItemY; + + this.scrollGroup.animate({ + translateY: scrollOffset + }); + + this.currentPage = currentPage; + this.positionCheckboxes(scrollOffset); + } + + } + + }; + + /* + * LegendSymbolMixin + */ + + var LegendSymbolMixin = Highcharts.LegendSymbolMixin = { + + /** + * Get the series' symbol in the legend + * + * @param {Object} legend The legend object + * @param {Object} item The series (this) or point + */ + drawRectangle: function (legend, item) { + var symbolHeight = legend.options.symbolHeight || legend.fontMetrics.f; + + item.legendSymbol = this.chart.renderer.rect( + 0, + legend.baseline - symbolHeight + 1, // #3988 + legend.symbolWidth, + symbolHeight, + legend.options.symbolRadius || 0 + ).attr({ + zIndex: 3 + }).add(item.legendGroup); + + }, + + /** + * Get the series' symbol in the legend. This method should be overridable to create custom + * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols. + * + * @param {Object} legend The legend object + */ + drawLineMarker: function (legend) { + + var options = this.options, + markerOptions = options.marker, + radius, + legendSymbol, + symbolWidth = legend.symbolWidth, + renderer = this.chart.renderer, + legendItemGroup = this.legendGroup, + verticalCenter = legend.baseline - mathRound(legend.fontMetrics.b * 0.3), + attr; + + // Draw the line + if (options.lineWidth) { + attr = { + 'stroke-width': options.lineWidth + }; + if (options.dashStyle) { + attr.dashstyle = options.dashStyle; + } + this.legendLine = renderer.path([ + M, + 0, + verticalCenter, + L, + symbolWidth, + verticalCenter + ]) + .attr(attr) + .add(legendItemGroup); + } + + // Draw the marker + if (markerOptions && markerOptions.enabled !== false) { + radius = markerOptions.radius; + this.legendSymbol = legendSymbol = renderer.symbol( + this.symbol, + (symbolWidth / 2) - radius, + verticalCenter - radius, + 2 * radius, + 2 * radius, + markerOptions + ) + .add(legendItemGroup); + legendSymbol.isMarker = true; + } + } + }; + + // Workaround for #2030, horizontal legend items not displaying in IE11 Preview, + // and for #2580, a similar drawing flaw in Firefox 26. + // Explore if there's a general cause for this. The problem may be related + // to nested group elements, as the legend item texts are within 4 group elements. + if (/Trident\/7\.0/.test(userAgent) || isFirefox) { + wrap(Legend.prototype, 'positionItem', function (proceed, item) { + var legend = this, + runPositionItem = function () { // If chart destroyed in sync, this is undefined (#2030) + if (item._legendItemPos) { + proceed.call(legend, item); + } + }; + + // Do it now, for export and to get checkbox placement + runPositionItem(); + + // Do it after to work around the core issue + setTimeout(runPositionItem); + }); + } + /** + * The Chart class + * @param {String|Object} renderTo The DOM element to render to, or its id + * @param {Object} options + * @param {Function} callback Function to run when the chart has loaded + */ + var Chart = Highcharts.Chart = function () { + this.getArgs.apply(this, arguments); + }; + + Highcharts.chart = function (a, b, c) { + return new Chart(a, b, c); + }; + + Chart.prototype = { + + /** + * Hook for modules + */ + callbacks: [], + + /** + * Handle the arguments passed to the constructor + * @returns {Array} Arguments without renderTo + */ + getArgs: function () { + var args = [].slice.call(arguments); + + // Remove the optional first argument, renderTo, and + // set it on this. + if (isString(args[0]) || args[0].nodeName) { + this.renderTo = args.shift(); + } + this.init(args[0], args[1]); + }, + + /** + * Initialize the chart + */ + init: function (userOptions, callback) { + + // Handle regular options + var options, + seriesOptions = userOptions.series; // skip merging data points to increase performance + + userOptions.series = null; + options = merge(defaultOptions, userOptions); // do the merge + options.series = userOptions.series = seriesOptions; // set back the series data + this.userOptions = userOptions; + + var optionsChart = options.chart; + + // Create margin & spacing array + this.margin = this.splashArray('margin', optionsChart); + this.spacing = this.splashArray('spacing', optionsChart); + + var chartEvents = optionsChart.events; + + //this.runChartClick = chartEvents && !!chartEvents.click; + this.bounds = { h: {}, v: {} }; // Pixel data bounds for touch zoom + + this.callback = callback; + this.isResizing = 0; + this.options = options; + //chartTitleOptions = UNDEFINED; + //chartSubtitleOptions = UNDEFINED; + + this.axes = []; + this.series = []; + this.hasCartesianSeries = optionsChart.showAxes; + //this.axisOffset = UNDEFINED; + //this.maxTicks = UNDEFINED; // handle the greatest amount of ticks on grouped axes + //this.inverted = UNDEFINED; + //this.loadingShown = UNDEFINED; + //this.container = UNDEFINED; + //this.chartWidth = UNDEFINED; + //this.chartHeight = UNDEFINED; + //this.marginRight = UNDEFINED; + //this.marginBottom = UNDEFINED; + //this.containerWidth = UNDEFINED; + //this.containerHeight = UNDEFINED; + //this.oldChartWidth = UNDEFINED; + //this.oldChartHeight = UNDEFINED; + + //this.renderTo = UNDEFINED; + //this.renderToClone = UNDEFINED; + + //this.spacingBox = UNDEFINED + + //this.legend = UNDEFINED; + + // Elements + //this.chartBackground = UNDEFINED; + //this.plotBackground = UNDEFINED; + //this.plotBGImage = UNDEFINED; + //this.plotBorder = UNDEFINED; + //this.loadingDiv = UNDEFINED; + //this.loadingSpan = UNDEFINED; + + var chart = this, + eventType; + + // Add the chart to the global lookup + chart.index = charts.length; + charts.push(chart); + chartCount++; + + // Set up auto resize + if (optionsChart.reflow !== false) { + addEvent(chart, 'load', function () { + chart.initReflow(); + }); + } + + // Chart event handlers + if (chartEvents) { + for (eventType in chartEvents) { + addEvent(chart, eventType, chartEvents[eventType]); + } + } + + chart.xAxis = []; + chart.yAxis = []; + + // Expose methods and variables + chart.animation = useCanVG ? false : pick(optionsChart.animation, true); + chart.pointCount = chart.colorCounter = chart.symbolCounter = 0; + + chart.firstRender(); + }, + + /** + * Initialize an individual series, called internally before render time + */ + initSeries: function (options) { + var chart = this, + optionsChart = chart.options.chart, + type = options.type || optionsChart.type || optionsChart.defaultSeriesType, + series, + constr = seriesTypes[type]; + + // No such series type + if (!constr) { + error(17, true); + } + + series = new constr(); + series.init(this, options); + return series; + }, + + /** + * Check whether a given point is within the plot area + * + * @param {Number} plotX Pixel x relative to the plot area + * @param {Number} plotY Pixel y relative to the plot area + * @param {Boolean} inverted Whether the chart is inverted + */ + isInsidePlot: function (plotX, plotY, inverted) { + var x = inverted ? plotY : plotX, + y = inverted ? plotX : plotY; + + return x >= 0 && + x <= this.plotWidth && + y >= 0 && + y <= this.plotHeight; + }, + + /** + * Redraw legend, axes or series based on updated data + * + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + redraw: function (animation) { + var chart = this, + axes = chart.axes, + series = chart.series, + pointer = chart.pointer, + legend = chart.legend, + redrawLegend = chart.isDirtyLegend, + hasStackedSeries, + hasDirtyStacks, + hasCartesianSeries = chart.hasCartesianSeries, + isDirtyBox = chart.isDirtyBox, + seriesLength = series.length, + i = seriesLength, + serie, + renderer = chart.renderer, + isHiddenChart = renderer.isHidden(), + afterRedraw = []; + + setAnimation(animation, chart); + + if (isHiddenChart) { + chart.cloneRenderTo(); + } + + // Adjust title layout (reflow multiline text) + chart.layOutTitles(); + + // link stacked series + while (i--) { + serie = series[i]; + + if (serie.options.stacking) { + hasStackedSeries = true; + + if (serie.isDirty) { + hasDirtyStacks = true; + break; + } + } + } + if (hasDirtyStacks) { // 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) { + if (serie.options.legendType === 'point') { + if (serie.updateTotals) { + serie.updateTotals(); + } + redrawLegend = true; + } + } + if (serie.isDirtyData) { + fireEvent(serie, 'updatedData'); + } + }); + + // handle added or removed series + if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed + // draw legend graphics + legend.render(); + + chart.isDirtyLegend = false; + } + + // reset stacks + if (hasStackedSeries) { + chart.getStacks(); + } + + + if (hasCartesianSeries) { + if (!chart.isResizing) { + + // reset maxTicks + chart.maxTicks = null; + + // set axes scales + each(axes, function (axis) { + axis.setScale(); + }); + } + } + + chart.getMargins(); // #3098 + + if (hasCartesianSeries) { + // If one axis is dirty, all axes must be redrawn (#792, #2169) + each(axes, function (axis) { + if (axis.isDirty) { + isDirtyBox = true; + } + }); + + // redraw axes + each(axes, function (axis) { + + // Fire 'afterSetExtremes' only if extremes are set + var key = axis.min + ',' + axis.max; + if (axis.extKey !== key) { // #821, #4452 + axis.extKey = key; + afterRedraw.push(function () { // prevent a recursive call to chart.redraw() (#1119) + fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751 + delete axis.eventArgs; + }); + } + if (isDirtyBox || hasStackedSeries) { + axis.redraw(); + } + }); + } + + // the plot areas size has changed + if (isDirtyBox) { + chart.drawChartBox(); + } + + + // redraw affected series + each(series, function (serie) { + if (serie.isDirty && serie.visible && + (!serie.isCartesian || serie.xAxis)) { // issue #153 + serie.redraw(); + } + }); + + // move tooltip or reset + if (pointer) { + pointer.reset(true); + } + + // redraw if canvas + renderer.draw(); + + // fire the event + fireEvent(chart, 'redraw'); + + if (isHiddenChart) { + chart.cloneRenderTo(true); + } + + // Fire callbacks that are put on hold until after the redraw + each(afterRedraw, function (callback) { + callback.call(); + }); + }, + + /** + * Get an axis, series or point object by id. + * @param id {String} The id as given in the configuration options + */ + get: function (id) { + var chart = this, + axes = chart.axes, + series = chart.series; + + var i, + j, + points; + + // 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++) { + points = series[i].points || []; + for (j = 0; j < points.length; j++) { + if (points[j].id === id) { + return points[j]; + } + } + } + return null; + }, + + /** + * Create the Axis instances based on the config options + */ + getAxes: function () { + var chart = this, + options = this.options, + xAxisOptions = options.xAxis = splat(options.xAxis || {}), + yAxisOptions = options.yAxis = splat(options.yAxis || {}), + optionsArray; + + // make sure the options are arrays and add some members + each(xAxisOptions, function (axis, i) { + axis.index = i; + axis.isX = true; + }); + + each(yAxisOptions, function (axis, i) { + axis.index = i; + }); + + // concatenate all axis options into one array + optionsArray = xAxisOptions.concat(yAxisOptions); + + each(optionsArray, function (axisOptions) { + new Axis(chart, axisOptions); // eslint-disable-line no-new + }); + }, + + + /** + * Get the currently selected points from all series + */ + getSelectedPoints: function () { + var points = []; + each(this.series, function (serie) { + points = points.concat(grep(serie.points || [], function (point) { + return point.selected; + })); + }); + return points; + }, + + /** + * Get the currently selected series + */ + getSelectedSeries: function () { + return grep(this.series, function (serie) { + return serie.selected; + }); + }, + + /** + * Show the title and subtitle of the chart + * + * @param titleOptions {Object} New title options + * @param subtitleOptions {Object} New subtitle options + * + */ + setTitle: function (titleOptions, subtitleOptions, redraw) { + var chart = this, + options = chart.options, + chartTitleOptions, + chartSubtitleOptions; + + chartTitleOptions = options.title = merge(options.title, titleOptions); + chartSubtitleOptions = options.subtitle = 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) { + chart[name] = title = title.destroy(); // remove old + } + + if (chartTitleOptions && chartTitleOptions.text && !title) { + chart[name] = chart.renderer.text( + chartTitleOptions.text, + 0, + 0, + chartTitleOptions.useHTML + ) + .attr({ + align: chartTitleOptions.align, + 'class': PREFIX + name, + zIndex: chartTitleOptions.zIndex || 4 + }) + .css(chartTitleOptions.style) + .add(); + + } + }); + chart.layOutTitles(redraw); + }, + + /** + * Lay out the chart titles and cache the full offset height for use in getMargins + */ + layOutTitles: function (redraw) { + var titleOffset = 0, + title = this.title, + subtitle = this.subtitle, + options = this.options, + titleOptions = options.title, + subtitleOptions = options.subtitle, + requiresDirtyBox, + renderer = this.renderer, + spacingBox = this.spacingBox; + + if (title) { + title + .css({ width: (titleOptions.width || spacingBox.width + titleOptions.widthAdjust) + PX }) + .align(extend({ + y: renderer.fontMetrics(titleOptions.style.fontSize, title).b - 3 + }, titleOptions), false, spacingBox); + + if (!titleOptions.floating && !titleOptions.verticalAlign) { + titleOffset = title.getBBox().height; + } + } + if (subtitle) { + subtitle + .css({ width: (subtitleOptions.width || spacingBox.width + subtitleOptions.widthAdjust) + PX }) + .align(extend({ + y: titleOffset + (titleOptions.margin - 13) + renderer.fontMetrics(subtitleOptions.style.fontSize, title).b + }, subtitleOptions), false, spacingBox); + + if (!subtitleOptions.floating && !subtitleOptions.verticalAlign) { + titleOffset = mathCeil(titleOffset + subtitle.getBBox().height); + } + } + + requiresDirtyBox = this.titleOffset !== titleOffset; + this.titleOffset = titleOffset; // used in getMargins + + if (!this.isDirtyBox && requiresDirtyBox) { + this.isDirtyBox = requiresDirtyBox; + // Redraw if necessary (#2719, #2744) + if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) { + this.redraw(); + } + } + }, + + /** + * Get chart width and height according to options and container size + */ + getChartSize: function () { + var chart = this, + optionsChart = chart.options.chart, + widthOption = optionsChart.width, + heightOption = optionsChart.height, + renderTo = chart.renderToClone || chart.renderTo; + + // Get inner width and height + if (!defined(widthOption)) { + chart.containerWidth = getStyle(renderTo, 'width'); + } + if (!defined(heightOption)) { + chart.containerHeight = getStyle(renderTo, 'height'); + } + + chart.chartWidth = mathMax(0, widthOption || chart.containerWidth || 600); // #1393, 1460 + chart.chartHeight = mathMax(0, pick(heightOption, + // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7: + chart.containerHeight > 19 ? chart.containerHeight : 400)); + }, + + /** + * Create a clone of the chart's renderTo div and place it outside the viewport to allow + * size computation on chart.render and chart.redraw + */ + cloneRenderTo: function (revert) { + var clone = this.renderToClone, + container = this.container; + + // Destroy the clone and bring the container back to the real renderTo div + if (revert) { + if (clone) { + this.renderTo.appendChild(container); + discardElement(clone); + delete this.renderToClone; + } + + // Set up the clone + } else { + if (container && container.parentNode === this.renderTo) { + this.renderTo.removeChild(container); // do not clone this + } + this.renderToClone = clone = this.renderTo.cloneNode(0); + css(clone, { + position: ABSOLUTE, + top: '-9999px', + display: 'block' // #833 + }); + if (clone.style.setProperty) { // #2631 + clone.style.setProperty('display', 'block', 'important'); + } + doc.body.appendChild(clone); + if (container) { + clone.appendChild(container); + } + } + }, + + /** + * Get the containing element, determine the size and create the inner container + * div to hold the chart + */ + getContainer: function () { + var chart = this, + container, + options = chart.options, + optionsChart = options.chart, + chartWidth, + chartHeight, + renderTo = chart.renderTo, + indexAttrName = 'data-highcharts-chart', + oldChartIndex, + Ren, + containerId = 'highcharts-' + idCounter++; + + if (!renderTo) { + chart.renderTo = renderTo = optionsChart.renderTo; + } + + if (isString(renderTo)) { + chart.renderTo = renderTo = doc.getElementById(renderTo); + } + + // Display an error if the renderTo is wrong + if (!renderTo) { + error(13, true); + } + + // If the container already holds a chart, destroy it. The check for hasRendered is there + // because web pages that are saved to disk from the browser, will preserve the data-highcharts-chart + // attribute and the SVG contents, but not an interactive chart. So in this case, + // charts[oldChartIndex] will point to the wrong chart if any (#2609). + oldChartIndex = pInt(attr(renderTo, indexAttrName)); + if (isNumber(oldChartIndex) && charts[oldChartIndex] && charts[oldChartIndex].hasRendered) { + charts[oldChartIndex].destroy(); + } + + // Make a reference to the chart from the div + attr(renderTo, indexAttrName, chart.index); + + // 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. The allowClone option is used in sparklines as a micro optimization, + // saving about 1-2 ms each chart. + if (!optionsChart.skipClone && !renderTo.offsetWidth) { + chart.cloneRenderTo(); + } + + // get the width and height + chart.getChartSize(); + chartWidth = chart.chartWidth; + chartHeight = chart.chartHeight; + + // create the inner container + chart.container = container = createElement(DIV, { + className: PREFIX + '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', + lineHeight: 'normal', // #427 + zIndex: 0, // #1072 + '-webkit-tap-highlight-color': 'rgba(0,0,0,0)' + }, optionsChart.style), + chart.renderToClone || renderTo + ); + + // cache the cursor (#1650) + chart._cursor = container.style.cursor; + + // Initialize the renderer + Ren = Highcharts[optionsChart.renderer] || Renderer; + chart.renderer = new Ren( + container, + chartWidth, + chartHeight, + optionsChart.style, + optionsChart.forExport, + options.exporting && options.exporting.allowHTML + ); + + if (useCanVG) { + // If we need canvg library, extend and configure the renderer + // to get the tracker for translating mouse events + chart.renderer.create(chart, container, chartWidth, chartHeight); + } + // Add a reference to the charts index + chart.renderer.chartIndex = chart.index; + }, + + /** + * 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 (skipAxes) { + var chart = this, + spacing = chart.spacing, + margin = chart.margin, + titleOffset = chart.titleOffset; + + chart.resetMargins(); + + // Adjust for title and subtitle + if (titleOffset && !defined(margin[0])) { + chart.plotTop = mathMax(chart.plotTop, titleOffset + chart.options.title.margin + spacing[0]); + } + + // Adjust for legend + if (chart.legend.display) { + chart.legend.adjustMargins(margin, spacing); + } + + // adjust for scroller + if (chart.extraBottomMargin) { + chart.marginBottom += chart.extraBottomMargin; + } + if (chart.extraTopMargin) { + chart.plotTop += chart.extraTopMargin; + } + if (!skipAxes) { + this.getAxisMargins(); + } + }, + + getAxisMargins: function () { + + var chart = this, + axisOffset = chart.axisOffset = [0, 0, 0, 0], // top, right, bottom, left + margin = chart.margin; + + // pre-render axes to get labels offset width + if (chart.hasCartesianSeries) { + each(chart.axes, function (axis) { + if (axis.visible) { + axis.getOffset(); + } + }); + } + + // Add the axis offsets + each(marginNames, function (m, side) { + if (!defined(margin[side])) { + chart[m] += axisOffset[side]; + } + }); + + chart.setChartSize(); + + }, + + /** + * Resize the chart to its container if size is not explicitly set + */ + reflow: function (e) { + var chart = this, + optionsChart = chart.options.chart, + renderTo = chart.renderTo, + width = optionsChart.width || getStyle(renderTo, 'width'), + height = optionsChart.height || getStyle(renderTo, 'height'), + target = e ? e.target : win; + + // Width and height checks for display:none. Target is doc in IE8 and Opera, + // win in Firefox, Chrome and IE9. + if (!chart.hasUserSize && !chart.isPrinting && width && height && (target === win || target === doc)) { // #1093 + if (width !== chart.containerWidth || height !== chart.containerHeight) { + clearTimeout(chart.reflowTimeout); + // When called from window.resize, e is set, else it's called directly (#2224) + chart.reflowTimeout = syncTimeout(function () { + if (chart.container) { // It may have been destroyed in the meantime (#1257) + chart.setSize(width, height, false); + chart.hasUserSize = null; + } + }, e ? 100 : 0); + } + chart.containerWidth = width; + chart.containerHeight = height; + } + }, + + /** + * Add the event handlers necessary for auto resizing + */ + initReflow: function () { + var chart = this, + reflow = function (e) { + chart.reflow(e); + }; + + + addEvent(win, 'resize', reflow); + addEvent(chart, 'destroy', function () { + removeEvent(win, 'resize', reflow); + }); + }, + + /** + * Resize the chart to a given width and height + * @param {Number} width + * @param {Number} height + * @param {Object|Boolean} animation + */ + setSize: function (width, height, animation) { + var chart = this, + chartWidth, + chartHeight, + renderer = chart.renderer, + globalAnimation; + + // Handle the isResizing counter + chart.isResizing += 1; + + // set the animation for the current process + setAnimation(animation, chart); + + chart.oldChartHeight = chart.chartHeight; + chart.oldChartWidth = chart.chartWidth; + if (defined(width)) { + chart.chartWidth = chartWidth = mathMax(0, mathRound(width)); + chart.hasUserSize = !!chartWidth; + } + if (defined(height)) { + chart.chartHeight = chartHeight = mathMax(0, mathRound(height)); + } + + // Resize the container with the global animation applied if enabled (#2503) + globalAnimation = renderer.globalAnimation; + (globalAnimation ? animate : css)(chart.container, { + width: chartWidth + PX, + height: chartHeight + PX + }, globalAnimation); + + chart.setChartSize(true); + renderer.setSize(chartWidth, chartHeight, animation); + + // handle axes + chart.maxTicks = null; + each(chart.axes, function (axis) { + axis.isDirty = true; + axis.setScale(); + }); + + // make sure non-cartesian series are also handled + each(chart.series, function (serie) { + serie.isDirty = true; + }); + + chart.isDirtyLegend = true; // force legend redraw + chart.isDirtyBox = true; // force redraw of plot and chart border + + chart.layOutTitles(); // #2857 + chart.getMargins(); + + chart.redraw(animation); + + + chart.oldChartHeight = null; + fireEvent(chart, 'resize'); + + // Fire endResize and set isResizing back. If animation is disabled, fire without delay + syncTimeout(function () { + if (chart) { + fireEvent(chart, 'endResize', null, function () { + chart.isResizing -= 1; + }); + } + }, animObject(globalAnimation).duration); + }, + + /** + * Set the public chart properties. This is done before and after the pre-render + * to determine margin sizes + */ + setChartSize: function (skipAxes) { + var chart = this, + inverted = chart.inverted, + renderer = chart.renderer, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + optionsChart = chart.options.chart, + spacing = chart.spacing, + clipOffset = chart.clipOffset, + clipX, + clipY, + plotLeft, + plotTop, + plotWidth, + plotHeight, + plotBorderWidth; + + chart.plotLeft = plotLeft = mathRound(chart.plotLeft); + chart.plotTop = plotTop = mathRound(chart.plotTop); + chart.plotWidth = plotWidth = mathMax(0, mathRound(chartWidth - plotLeft - chart.marginRight)); + chart.plotHeight = plotHeight = mathMax(0, mathRound(chartHeight - plotTop - chart.marginBottom)); + + chart.plotSizeX = inverted ? plotHeight : plotWidth; + chart.plotSizeY = inverted ? plotWidth : plotHeight; + + chart.plotBorderWidth = optionsChart.plotBorderWidth || 0; + + // Set boxes used for alignment + chart.spacingBox = renderer.spacingBox = { + x: spacing[3], + y: spacing[0], + width: chartWidth - spacing[3] - spacing[1], + height: chartHeight - spacing[0] - spacing[2] + }; + chart.plotBox = renderer.plotBox = { + x: plotLeft, + y: plotTop, + width: plotWidth, + height: plotHeight + }; + + plotBorderWidth = 2 * mathFloor(chart.plotBorderWidth / 2); + clipX = mathCeil(mathMax(plotBorderWidth, clipOffset[3]) / 2); + clipY = mathCeil(mathMax(plotBorderWidth, clipOffset[0]) / 2); + chart.clipBox = { + x: clipX, + y: clipY, + width: mathFloor(chart.plotSizeX - mathMax(plotBorderWidth, clipOffset[1]) / 2 - clipX), + height: mathMax(0, mathFloor(chart.plotSizeY - mathMax(plotBorderWidth, clipOffset[2]) / 2 - clipY)) + }; + + if (!skipAxes) { + each(chart.axes, function (axis) { + axis.setAxisSize(); + axis.setAxisTranslation(); + }); + } + }, + + /** + * Initial margins before auto size margins are applied + */ + resetMargins: function () { + var chart = this; + + each(marginNames, function (m, side) { + chart[m] = pick(chart.margin[side], chart.spacing[side]); + }); + chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left + chart.clipOffset = [0, 0, 0, 0]; + }, + + /** + * Draw the borders and backgrounds for chart and plot area + */ + drawChartBox: function () { + var chart = this, + optionsChart = chart.options.chart, + renderer = chart.renderer, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + chartBackground = chart.chartBackground, + plotBackground = chart.plotBackground, + plotBorder = chart.plotBorder, + plotBGImage = chart.plotBGImage, + chartBorderWidth = optionsChart.borderWidth || 0, + chartBackgroundColor = optionsChart.backgroundColor, + plotBackgroundColor = optionsChart.plotBackgroundColor, + plotBackgroundImage = optionsChart.plotBackgroundImage, + plotBorderWidth = optionsChart.plotBorderWidth || 0, + mgn, + bgAttr, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + plotBox = chart.plotBox, + clipRect = chart.clipRect, + clipBox = chart.clipBox; + + // Chart area + mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0); + + if (chartBorderWidth || chartBackgroundColor) { + if (!chartBackground) { + + bgAttr = { + fill: chartBackgroundColor || NONE + }; + if (chartBorderWidth) { // #980 + bgAttr.stroke = optionsChart.borderColor; + bgAttr['stroke-width'] = chartBorderWidth; + } + chart.chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn, + optionsChart.borderRadius, chartBorderWidth) + .attr(bgAttr) + .addClass(PREFIX + 'background') + .add() + .shadow(optionsChart.shadow); + + } else { // resize + chartBackground.animate( + chartBackground.crisp({ width: chartWidth - mgn, height: chartHeight - mgn }) + ); + } + } + + + // Plot background + if (plotBackgroundColor) { + if (!plotBackground) { + chart.plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0) + .attr({ + fill: plotBackgroundColor + }) + .add() + .shadow(optionsChart.plotShadow); + } else { + plotBackground.animate(plotBox); + } + } + if (plotBackgroundImage) { + if (!plotBGImage) { + chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight) + .add(); + } else { + plotBGImage.animate(plotBox); + } + } + + // Plot clip + if (!clipRect) { + chart.clipRect = renderer.clipRect(clipBox); + } else { + clipRect.animate({ + width: clipBox.width, + height: clipBox.height + }); + } + + // Plot area border + if (plotBorderWidth) { + if (!plotBorder) { + chart.plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, -plotBorderWidth) + .attr({ + stroke: optionsChart.plotBorderColor, + 'stroke-width': plotBorderWidth, + fill: NONE, + zIndex: 1 + }) + .add(); + } else { + plotBorder.strokeWidth = -plotBorderWidth; + plotBorder.animate( + plotBorder.crisp({ x: plotLeft, y: plotTop, width: plotWidth, height: plotHeight }) //#3282 plotBorder should be negative + ); + } + } + + // reset + chart.isDirtyBox = false; + }, + + /** + * Detect whether a certain chart property is needed based on inspecting its options + * and series. This mainly applies to the chart.invert property, and in extensions to + * the chart.angular and chart.polar properties. + */ + propFromSeries: function () { + var chart = this, + optionsChart = chart.options.chart, + klass, + seriesOptions = chart.options.series, + i, + value; + + + each(['inverted', 'angular', 'polar'], function (key) { + + // The default series type's class + klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType]; + + // Get the value from available chart-wide properties + value = ( + chart[key] || // 1. it is set before + optionsChart[key] || // 2. it is set in the options + (klass && klass.prototype[key]) // 3. it's default series class requires it + ); + + // 4. Check if any the chart's series require it + i = seriesOptions && seriesOptions.length; + while (!value && i--) { + klass = seriesTypes[seriesOptions[i].type]; + if (klass && klass.prototype[key]) { + value = true; + } + } + + // Set the chart property + chart[key] = value; + }); + + }, + + /** + * Link two or more series together. This is done initially from Chart.render, + * and after Chart.addSeries and Series.remove. + */ + linkSeries: function () { + var chart = this, + chartSeries = chart.series; + + // Reset links + each(chartSeries, function (series) { + series.linkedSeries.length = 0; + }); + + // Apply new links + each(chartSeries, function (series) { + var linkedTo = series.options.linkedTo; + if (isString(linkedTo)) { + if (linkedTo === ':previous') { + linkedTo = chart.series[series.index - 1]; + } else { + linkedTo = chart.get(linkedTo); + } + if (linkedTo) { + linkedTo.linkedSeries.push(series); + series.linkedParent = linkedTo; + series.visible = pick(series.options.visible, linkedTo.options.visible, series.visible); // #3879 + } + } + }); + }, + + /** + * Render series for the chart + */ + renderSeries: function () { + each(this.series, function (serie) { + serie.translate(); + serie.render(); + }); + }, + + /** + * Render labels for the chart + */ + renderLabels: function () { + var chart = this, + labels = chart.options.labels; + if (labels.items) { + each(labels.items, function (label) { + var style = extend(labels.style, label.style), + x = pInt(style.left) + chart.plotLeft, + y = pInt(style.top) + chart.plotTop + 12; + + // delete to prevent rewriting in IE + delete style.left; + delete style.top; + + chart.renderer.text( + label.html, + x, + y + ) + .attr({ zIndex: 2 }) + .css(style) + .add(); + + }); + } + }, + + /** + * Render all graphics for the chart + */ + render: function () { + var chart = this, + axes = chart.axes, + renderer = chart.renderer, + options = chart.options, + tempWidth, + tempHeight, + redoHorizontal, + redoVertical; + + // Title + chart.setTitle(); + + + // Legend + chart.legend = new Legend(chart, options.legend); + + // Get stacks + if (chart.getStacks) { + chart.getStacks(); + } + + // Get chart margins + chart.getMargins(true); + chart.setChartSize(); + + // Record preliminary dimensions for later comparison + tempWidth = chart.plotWidth; + tempHeight = chart.plotHeight = chart.plotHeight - 21; // 21 is the most common correction for X axis labels + + // Get margins by pre-rendering axes + each(axes, function (axis) { + axis.setScale(); + }); + chart.getAxisMargins(); + + // If the plot area size has changed significantly, calculate tick positions again + redoHorizontal = tempWidth / chart.plotWidth > 1.1; + redoVertical = tempHeight / chart.plotHeight > 1.05; // Height is more sensitive + + if (redoHorizontal || redoVertical) { + + chart.maxTicks = null; // reset for second pass + each(axes, function (axis) { + if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) { + axis.setTickInterval(true); // update to reflect the new margins + } + }); + chart.getMargins(); // second pass to check for new labels + } + + // Draw the borders and backgrounds + chart.drawChartBox(); + + + // Axes + if (chart.hasCartesianSeries) { + each(axes, function (axis) { + if (axis.visible) { + axis.render(); + } + }); + } + + // The series + if (!chart.seriesGroup) { + chart.seriesGroup = renderer.g('series-group') + .attr({ zIndex: 3 }) + .add(); + } + chart.renderSeries(); + + // Labels + chart.renderLabels(); + + // Credits + chart.showCredits(options.credits); + + // Set flag + chart.hasRendered = true; + + }, + + /** + * Show chart credits based on config options + */ + showCredits: function (credits) { + if (credits.enabled && !this.credits) { + this.credits = this.renderer.text( + credits.text, + 0, + 0 + ) + .on('click', function () { + if (credits.href) { + win.location.href = credits.href; + } + }) + .attr({ + align: credits.position.align, + zIndex: 8 + }) + .css(credits.style) + .add() + .align(credits.position); + } + }, + + /** + * Clean up memory usage + */ + destroy: function () { + var chart = this, + axes = chart.axes, + series = chart.series, + container = chart.container, + i, + parentNode = container && container.parentNode; + + // fire the chart.destoy event + fireEvent(chart, 'destroy'); + + // Delete the chart from charts lookup array + charts[chart.index] = UNDEFINED; + chartCount--; + chart.renderTo.removeAttribute('data-highcharts-chart'); + + // remove events + 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', 'chartBackground', 'plotBackground', 'plotBGImage', + 'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer', 'scroller', + 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'], function (name) { + var prop = chart[name]; + + if (prop && prop.destroy) { + chart[name] = prop.destroy(); + } + }); + + // remove container and all SVG + if (container) { // can break in IE when destroyed before finished loading + container.innerHTML = ''; + removeEvent(container); + if (parentNode) { + discardElement(container); + } + + } + + // clean it all up + for (i in chart) { + delete chart[i]; + } + + }, + + + /** + * VML namespaces can't be added until after complete. Listening + * for Perini's doScroll hack is not enough. + */ + isReadyToRender: function () { + var chart = this; + + // Note: win == win.top is required + if ((!hasSVG && (win == win.top && doc.readyState !== 'complete')) || (useCanVG && !win.canvg)) { // eslint-disable-line eqeqeq + if (useCanVG) { + // Delay rendering until canvg library is downloaded and ready + CanVGController.push(function () { + chart.firstRender(); + }, chart.options.global.canvasToolsURL); + } else { + doc.attachEvent('onreadystatechange', function () { + doc.detachEvent('onreadystatechange', chart.firstRender); + if (doc.readyState === 'complete') { + chart.firstRender(); + } + }); + } + return false; + } + return true; + }, + + /** + * Prepare for first rendering after all data are loaded + */ + firstRender: function () { + var chart = this, + options = chart.options; + + // Check whether the chart is ready to render + if (!chart.isReadyToRender()) { + return; + } + + // Create the container + chart.getContainer(); + + // Run an early event after the container and renderer are established + fireEvent(chart, 'init'); + + + chart.resetMargins(); + chart.setChartSize(); + + // Set the common chart properties (mainly invert) from the given series + chart.propFromSeries(); + + // get axes + chart.getAxes(); + + // Initialize the series + each(options.series || [], function (serieOptions) { + chart.initSeries(serieOptions); + }); + + chart.linkSeries(); + + // Run an event after axes and series are initialized, but before render. At this stage, + // the series data is indexed and cached in the xData and yData arrays, so we can access + // those before rendering. Used in Highstock. + fireEvent(chart, 'beforeRender'); + + // depends on inverted and on margins being set + if (Highcharts.Pointer) { + chart.pointer = new Pointer(chart, options); + } + + chart.render(); + + // add canvas + chart.renderer.draw(); + + // Fire the load event if there are no external images + if (!chart.renderer.imgCount && chart.onload) { + chart.onload(); + } + + // If the chart was rendered outside the top container, put it back in (#3679) + chart.cloneRenderTo(true); + + }, + + /** + * On chart load + */ + onload: function () { + var chart = this; + + // Run callbacks + each([this.callback].concat(this.callbacks), function (fn) { + if (fn && chart.index !== undefined) { // Chart destroyed in its own callback (#3600) + fn.apply(chart, [chart]); + } + }); + + fireEvent(chart, 'load'); + + // Don't run again + this.onload = null; + }, + + /** + * Creates arrays for spacing and margin from given options. + */ + splashArray: function (target, options) { + var oVar = options[target], + tArray = isObject(oVar) ? oVar : [oVar, oVar, oVar, oVar]; + + return [pick(options[target + 'Top'], tArray[0]), + pick(options[target + 'Right'], tArray[1]), + pick(options[target + 'Bottom'], tArray[2]), + pick(options[target + 'Left'], tArray[3])]; + } + }; // end Chart + + var CenteredSeriesMixin = Highcharts.CenteredSeriesMixin = { + /** + * Get the center of the pie based on the size and center options relative to the + * plot area. Borrowed by the polar and gauge series types. + */ + getCenter: function () { + + var options = this.options, + chart = this.chart, + slicingRoom = 2 * (options.slicedOffset || 0), + handleSlicingRoom, + plotWidth = chart.plotWidth - 2 * slicingRoom, + plotHeight = chart.plotHeight - 2 * slicingRoom, + centerOption = options.center, + positions = [pick(centerOption[0], '50%'), pick(centerOption[1], '50%'), options.size || '100%', options.innerSize || 0], + smallestSize = mathMin(plotWidth, plotHeight), + i, + value; + + for (i = 0; i < 4; ++i) { + value = positions[i]; + handleSlicingRoom = i < 2 || (i === 2 && /%$/.test(value)); + + // i == 0: centerX, relative to width + // i == 1: centerY, relative to height + // i == 2: size, relative to smallestSize + // i == 3: innerSize, relative to size + positions[i] = relativeLength(value, [plotWidth, plotHeight, smallestSize, positions[2]][i]) + + (handleSlicingRoom ? slicingRoom : 0); + + } + // innerSize cannot be larger than size (#3632) + if (positions[3] > positions[2]) { + positions[3] = positions[2]; + } + return positions; + } + }; + + /** + * 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, x) { + + var point = this, + colors; + point.series = series; + point.color = series.color; // #3445 + point.applyOptions(options, x); + point.pointAttr = {}; + + if (series.options.colorByPoint) { + colors = series.options.colors || series.chart.options.colors; + point.color = point.color || colors[series.colorCounter++]; + // loop back to zero + if (series.colorCounter === colors.length) { + series.colorCounter = 0; + } + } + + series.chart.pointCount++; + return point; + }, + /** + * Apply the options containing the x and y data and possible some extra properties. + * This is called on point init or from point.update. + * + * @param {Object} options + */ + applyOptions: function (options, x) { + var point = this, + series = point.series, + pointValKey = series.options.pointValKey || series.pointValKey; + + options = Point.prototype.optionsToObject.call(this, options); + + // copy options directly to point + extend(point, options); + point.options = point.options ? extend(point.options, options) : options; + + // For higher dimension series types. For instance, for ranges, point.y is mapped to point.low. + if (pointValKey) { + point.y = point[pointValKey]; + } + point.isNull = point.x === null || point.y === null; + + // 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 && series) { + point.x = x === undefined ? series.autoIncrement() : x; + } + + return point; + }, + + /** + * Transform number or array configs into objects + */ + optionsToObject: function (options) { + var ret = {}, + series = this.series, + keys = series.options.keys, + pointArrayMap = keys || series.pointArrayMap || ['y'], + valueCount = pointArrayMap.length, + firstItemType, + i = 0, + j = 0; + + if (isNumber(options) || options === null) { + ret[pointArrayMap[0]] = options; + + } else if (isArray(options)) { + // with leading x value + if (!keys && options.length > valueCount) { + firstItemType = typeof options[0]; + if (firstItemType === 'string') { + ret.name = options[0]; + } else if (firstItemType === 'number') { + ret.x = options[0]; + } + i++; + } + while (j < valueCount) { + if (!keys || options[i] !== undefined) { // Skip undefined positions for keys + ret[pointArrayMap[j]] = options[i]; + } + i++; + j++; + } + } else if (typeof options === 'object') { + ret = options; + + // This is the fastest way to detect if there are individual point dataLabels that need + // to be considered in drawDataLabels. These can only occur in object configs. + if (options.dataLabels) { + series._hasPointLabels = true; + } + + // Same approach as above for markers + if (options.marker) { + series._hasPointMarkers = true; + } + } + return ret; + }, + + /** + * Destroy a point to clear memory. Its reference still stays in series.data. + */ + destroy: function () { + var point = this, + series = point.series, + chart = series.chart, + hoverPoints = chart.hoverPoints, + prop; + + chart.pointCount--; + + if (hoverPoints) { + point.setState(); + erase(hoverPoints, point); + if (!hoverPoints.length) { + chart.hoverPoints = null; + } + + } + if (point === chart.hoverPoint) { + point.onMouseOut(); + } + + // remove all events + if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive + removeEvent(point); + point.destroyElements(); + } + + if (point.legendItem) { // pies have legend items + chart.legend.destroyItem(point); + } + + for (prop in point) { + point[prop] = null; + } + + + }, + + /** + * Destroy SVG elements associated with the point + */ + destroyElements: function () { + var point = this, + props = ['graphic', 'dataLabel', 'dataLabelUpper', 'connector', 'shadowGroup'], + prop, + i = 6; + while (i--) { + prop = props[i]; + if (point[prop]) { + point[prop] = point[prop].destroy(); + } + } + }, + + /** + * Return the configuration hash needed for the data label and tooltip formatters + */ + getLabelConfig: function () { + return { + x: this.category, + y: this.y, + color: this.color, + key: this.name || this.category, + series: this.series, + point: this, + percentage: this.percentage, + total: this.total || this.stackTotal + }; + }, + + /** + * Extendable method for formatting each point's tooltip line + * + * @return {String} A string to be concatenated in to the common tooltip text + */ + tooltipFormatter: function (pointFormat) { + + // Insert options for valueDecimals, valuePrefix, and valueSuffix + var series = this.series, + seriesTooltipOptions = series.tooltipOptions, + valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''), + valuePrefix = seriesTooltipOptions.valuePrefix || '', + valueSuffix = seriesTooltipOptions.valueSuffix || ''; + + // Loop over the point array map and replace unformatted values with sprintf formatting markup + each(series.pointArrayMap || ['y'], function (key) { + key = '{point.' + key; // without the closing bracket + if (valuePrefix || valueSuffix) { + pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix); + } + pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}'); + }); + + return format(pointFormat, { + point: this, + series: this.series + }); + }, + + /** + * Fire an event on the Point object. + * @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 + if (point.select) { // Could be destroyed by prior event handlers (#2911) + point.select(null, event.ctrlKey || event.metaKey || event.shiftKey); + } + }; + } + + fireEvent(this, eventType, eventArgs, defaultFunction); + }, + visible: true + };/** + * @classDescription The base function which all other series types inherit from. The data in the series is stored + * in various arrays. + * + * - First, series.options.data contains all the original config options for + * each point whether added by options or methods like series.addPoint. + * - Next, series.data contains those values converted to points, but in case the series data length + * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It + * only contains the points that have been created on demand. + * - Then there's series.points that contains all currently visible point objects. In case of cropping, + * the cropped-away points are not part of this array. The series.points array starts at series.cropStart + * compared to series.data and series.options.data. If however the series data is grouped, these can't + * be correlated one to one. + * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points. + * - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points. + * + * @param {Object} chart + * @param {Object} options + */ + var Series = Highcharts.Series = function () {}; + + Series.prototype = { + + isCartesian: true, + type: 'line', + pointClass: Point, + sorted: true, // requires the data to be sorted + requireSorting: true, + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'lineColor', + 'stroke-width': 'lineWidth', + fill: 'fillColor', + r: 'radius' + }, + directTouch: false, + axisTypes: ['xAxis', 'yAxis'], + colorCounter: 0, + parallelArrays: ['x', 'y'], // each point's x and y values are stored in this.xData and this.yData + init: function (chart, options) { + var series = this, + eventType, + events, + chartSeries = chart.series, + sortByIndex = function (a, b) { + return pick(a.options.index, a._i) - pick(b.options.index, b._i); + }; + + series.chart = chart; + series.options = options = series.setOptions(options); // merge with plotOptions + series.linkedSeries = []; + + // bind the axes + series.bindAxes(); + + // set some variables + extend(series, { + name: options.name, + state: NORMAL_STATE, + pointAttr: {}, + visible: options.visible !== false, // true by default + selected: options.selected === true // false by default + }); + + // special + if (useCanVG) { + options.animation = false; + } + + // 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 + each(series.parallelArrays, function (key) { + series[key + 'Data'] = []; + }); + series.setData(options.data, false); + + // Mark cartesian + if (series.isCartesian) { + chart.hasCartesianSeries = true; + } + + // Register it in the chart + chartSeries.push(series); + series._i = chartSeries.length - 1; + + // Sort series according to index option (#248, #1123, #2456) + stableSort(chartSeries, sortByIndex); + if (this.yAxis) { + stableSort(this.yAxis.series, sortByIndex); + } + + each(chartSeries, function (series, i) { + series.index = i; + series.name = series.name || 'Series ' + (i + 1); + }); + + }, + + /** + * Set the xAxis and yAxis properties of cartesian series, and register the series + * in the axis.series array + */ + bindAxes: function () { + var series = this, + seriesOptions = series.options, + chart = series.chart, + axisOptions; + + each(series.axisTypes || [], function (AXIS) { // repeat for xAxis and yAxis + + each(chart[AXIS], function (axis) { // loop through the chart's axis objects + axisOptions = axis.options; + + // apply if the series xAxis or yAxis option mathches the number of the + // axis, or if undefined, use the first axis + if ((seriesOptions[AXIS] === axisOptions.index) || + (seriesOptions[AXIS] !== UNDEFINED && seriesOptions[AXIS] === axisOptions.id) || + (seriesOptions[AXIS] === UNDEFINED && axisOptions.index === 0)) { + + // register this series in the axis.series lookup + axis.series.push(series); + + // set this series.xAxis or series.yAxis reference + series[AXIS] = axis; + + // mark dirty for redraw + axis.isDirty = true; + } + }); + + // The series needs an X and an Y axis + if (!series[AXIS] && series.optionalAxis !== AXIS) { + error(18, true); + } + + }); + }, + + /** + * For simple series types like line and column, the data values are held in arrays like + * xData and yData for quick lookup to find extremes and more. For multidimensional series + * like bubble and map, this can be extended with arrays like zData and valueData by + * adding to the series.parallelArrays array. + */ + updateParallelArrays: function (point, i) { + var series = point.series, + args = arguments, + fn = isNumber(i) ? + // Insert the value in the given position + function (key) { + var val = key === 'y' && series.toYData ? series.toYData(point) : point[key]; + series[key + 'Data'][i] = val; + } : + // Apply the method specified in i with the following arguments as arguments + function (key) { + Array.prototype[i].apply(series[key + 'Data'], Array.prototype.slice.call(args, 2)); + }; + + each(series.parallelArrays, fn); + }, + + /** + * 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 options = this.options, + xIncrement = this.xIncrement, + date, + pointInterval, + pointIntervalUnit = options.pointIntervalUnit; + + xIncrement = pick(xIncrement, options.pointStart, 0); + + this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1); + + // Added code for pointInterval strings + if (pointIntervalUnit) { + date = new Date(xIncrement); + + if (pointIntervalUnit === 'day') { + date = +date[setDate](date[getDate]() + pointInterval); + } else if (pointIntervalUnit === 'month') { + date = +date[setMonth](date[getMonth]() + pointInterval); + } else if (pointIntervalUnit === 'year') { + date = +date[setFullYear](date[getFullYear]() + pointInterval); + } + pointInterval = date - xIncrement; + } + + this.xIncrement = xIncrement + pointInterval; + return xIncrement; + }, + + /** + * Set the series options by merging from the options tree + * @param {Object} itemOptions + */ + setOptions: function (itemOptions) { + var chart = this.chart, + chartOptions = chart.options, + plotOptions = chartOptions.plotOptions, + userOptions = chart.userOptions || {}, + userPlotOptions = userOptions.plotOptions || {}, + typeOptions = plotOptions[this.type], + options, + zones; + + this.userOptions = itemOptions; + + // General series options take precedence over type options because otherwise, default + // type options like column.animation would be overwritten by the general option. + // But issues have been raised here (#3881), and the solution may be to distinguish + // between default option and userOptions like in the tooltip below. + options = merge( + typeOptions, + plotOptions.series, + itemOptions + ); + + // The tooltip options are merged between global and series specific options + this.tooltipOptions = merge( + defaultOptions.tooltip, + defaultOptions.plotOptions[this.type].tooltip, + userOptions.tooltip, + userPlotOptions.series && userPlotOptions.series.tooltip, + userPlotOptions[this.type] && userPlotOptions[this.type].tooltip, + itemOptions.tooltip + ); + + // Delete marker object if not allowed (#1125) + if (typeOptions.marker === null) { + delete options.marker; + } + + // Handle color zones + this.zoneAxis = options.zoneAxis; + zones = this.zones = (options.zones || []).slice(); + if ((options.negativeColor || options.negativeFillColor) && !options.zones) { + zones.push({ + value: options[this.zoneAxis + 'Threshold'] || options.threshold || 0, + color: options.negativeColor, + fillColor: options.negativeFillColor + }); + } + if (zones.length) { // Push one extra zone for the rest + if (defined(zones[zones.length - 1].value)) { + zones.push({ + color: this.color, + fillColor: this.fillColor + }); + } + } + return options; + }, + + getCyclic: function (prop, value, defaults) { + var i, + userOptions = this.userOptions, + indexName = '_' + prop + 'Index', + counterName = prop + 'Counter'; + + if (!value) { + if (defined(userOptions[indexName])) { // after Series.update() + i = userOptions[indexName]; + } else { + userOptions[indexName] = i = this.chart[counterName] % defaults.length; + this.chart[counterName] += 1; + } + value = defaults[i]; + } + this[prop] = value; + }, + + /** + * Get the series' color + */ + getColor: function () { + if (this.options.colorByPoint) { + this.options.color = null; // #4359, selected slice got series.color even when colorByPoint was set. + } else { + this.getCyclic('color', this.options.color || defaultPlotOptions[this.type].color, this.chart.options.colors); + } + }, + /** + * Get the series' symbol + */ + getSymbol: function () { + var seriesMarkerOption = this.options.marker; + + this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols); + + // don't substract radius in image symbols (#604) + if (/^url/.test(this.symbol)) { + seriesMarkerOption.radius = 0; + } + }, + + drawLegendSymbol: LegendSymbolMixin.drawLineMarker, + + /** + * Replace the series data with a new set of data + * @param {Object} data + * @param {Object} redraw + */ + setData: function (data, redraw, animation, updatePoints) { + var series = this, + oldData = series.points, + oldDataLength = (oldData && oldData.length) || 0, + dataLength, + options = series.options, + chart = series.chart, + firstPoint = null, + xAxis = series.xAxis, + hasCategories = xAxis && !!xAxis.categories, + i, + turboThreshold = options.turboThreshold, + pt, + xData = this.xData, + yData = this.yData, + pointArrayMap = series.pointArrayMap, + valueCount = pointArrayMap && pointArrayMap.length; + + data = data || []; + dataLength = data.length; + redraw = pick(redraw, true); + + // If the point count is the same as is was, just run Point.update which is + // cheaper, allows animation, and keeps references to points. + if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData && series.visible) { + each(data, function (point, i) { + // .update doesn't exist on a linked, hidden series (#3709) + if (oldData[i].update && point !== options.data[i]) { + oldData[i].update(point, false, null, false); + } + }); + + } else { + + // Reset properties + series.xIncrement = null; + + series.colorCounter = 0; // for series with colorByPoint (#1547) + + // Update parallel arrays + each(this.parallelArrays, function (key) { + series[key + 'Data'].length = 0; + }); + + // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The + // first value is tested, and we assume that all the rest are defined the same + // way. Although the 'for' loops are similar, they are repeated inside each + // if-else conditional for max performance. + if (turboThreshold && dataLength > turboThreshold) { + + // find the first non-null point + i = 0; + while (firstPoint === null && i < dataLength) { + firstPoint = data[i]; + i++; + } + + + if (isNumber(firstPoint)) { // assume all points are numbers + var x = pick(options.pointStart, 0), + pointInterval = pick(options.pointInterval, 1); + + for (i = 0; i < dataLength; i++) { + xData[i] = x; + yData[i] = data[i]; + x += pointInterval; + } + series.xIncrement = x; + } else if (isArray(firstPoint)) { // assume all points are arrays + if (valueCount) { // [x, low, high] or [x, o, h, l, c] + for (i = 0; i < dataLength; i++) { + pt = data[i]; + xData[i] = pt[0]; + yData[i] = pt.slice(1, valueCount + 1); + } + } else { // [x, y] + for (i = 0; i < dataLength; i++) { + pt = data[i]; + xData[i] = pt[0]; + yData[i] = pt[1]; + } + } + } else { + error(12); // Highcharts expects configs to be numbers or arrays in turbo mode + } + } else { + for (i = 0; i < dataLength; i++) { + if (data[i] !== UNDEFINED) { // stray commas in oldIE + pt = { series: series }; + series.pointClass.prototype.applyOptions.apply(pt, [data[i]]); + series.updateParallelArrays(pt, i); + if (hasCategories && defined(pt.name)) { // #4401 + xAxis.names[pt.x] = pt.name; // #2046 + } + } + } + } + + // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON + if (isString(yData[0])) { + error(14, true); + } + + series.data = []; + series.options.data = series.userOptions.data = data; + + // destroy old points + i = oldDataLength; + while (i--) { + if (oldData[i] && oldData[i].destroy) { + oldData[i].destroy(); + } + } + + // reset minRange (#878) + if (xAxis) { + xAxis.minRange = xAxis.userMinRange; + } + + // redraw + series.isDirty = series.isDirtyData = chart.isDirtyBox = true; + animation = false; + } + + // Typically for pie series, points need to be processed and generated + // prior to rendering the legend + if (options.legendType === 'point') { + this.processData(); + this.generatePoints(); + } + + if (redraw) { + chart.redraw(animation); + } + }, + + /** + * Process the data by cropping away unused data points if the series is longer + * than the crop threshold. This saves computing time for lage series. + */ + processData: function (force) { + var series = this, + processedXData = series.xData, // copied during slice operation below + processedYData = series.yData, + dataLength = processedXData.length, + croppedData, + cropStart = 0, + cropped, + distance, + closestPointRange, + xAxis = series.xAxis, + i, // loop variable + options = series.options, + cropThreshold = options.cropThreshold, + getExtremesFromAll = series.getExtremesFromAll || options.getExtremesFromAll, // #4599 + isCartesian = series.isCartesian, + xExtremes, + val2lin = xAxis && xAxis.val2lin, + isLog = xAxis && xAxis.isLog, + min, + max; + + // If the series data or axes haven't changed, don't go through this. Return false to pass + // the message on to override methods like in data grouping. + if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) { + return false; + } + + if (xAxis) { + xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053) + min = xExtremes.min; + max = xExtremes.max; + } + + // optionally filter out points outside the plot area + if (isCartesian && series.sorted && !getExtremesFromAll && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) { + + // it's outside current extremes + if (processedXData[dataLength - 1] < min || processedXData[0] > max) { + processedXData = []; + processedYData = []; + + // only crop if it's actually spilling out + } else if (processedXData[0] < min || processedXData[dataLength - 1] > max) { + croppedData = this.cropData(series.xData, series.yData, min, max); + processedXData = croppedData.xData; + processedYData = croppedData.yData; + cropStart = croppedData.start; + cropped = true; + } + } + + + // Find the closest distance between processed points + i = processedXData.length || 1; + while (--i) { + distance = isLog ? + val2lin(processedXData[i]) - val2lin(processedXData[i - 1]) : + processedXData[i] - processedXData[i - 1]; + + if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) { + closestPointRange = distance; + + // Unsorted data is not supported by the line tooltip, as well as data grouping and + // navigation in Stock charts (#725) and width calculation of columns (#1900) + } else if (distance < 0 && series.requireSorting) { + error(15); + } + } + + // Record the properties + series.cropped = cropped; // undefined or true + series.cropStart = cropStart; + series.processedXData = processedXData; + series.processedYData = processedYData; + + series.closestPointRange = closestPointRange; + + }, + + /** + * Iterate over xData and crop values between min and max. Returns object containing crop start/end + * cropped xData with corresponding part of yData, dataMin and dataMax within the cropped range + */ + cropData: function (xData, yData, min, max) { + var dataLength = xData.length, + cropStart = 0, + cropEnd = dataLength, + cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside + i, + j; + + // iterate up to find slice start + for (i = 0; i < dataLength; i++) { + if (xData[i] >= min) { + cropStart = mathMax(0, i - cropShoulder); + break; + } + } + + // proceed to find slice end + for (j = i; j < dataLength; j++) { + if (xData[j] > max) { + cropEnd = j + cropShoulder; + break; + } + } + + return { + xData: xData.slice(cropStart, cropEnd), + yData: yData.slice(cropStart, cropEnd), + start: cropStart, + end: cropEnd + }; + }, + + + /** + * Generate the data point after the data has been processed by cropping away + * unused points and optionally grouped in Highcharts Stock. + */ + generatePoints: function () { + var series = this, + options = series.options, + dataOptions = options.data, + data = series.data, + dataLength, + processedXData = series.processedXData, + processedYData = series.processedYData, + pointClass = series.pointClass, + processedDataLength = processedXData.length, + cropStart = series.cropStart || 0, + cursor, + hasGroupedData = series.hasGroupedData, + point, + points = [], + i; + + if (!data && !hasGroupedData) { + var arr = []; + arr.length = dataOptions.length; + data = series.data = arr; + } + + for (i = 0; i < processedDataLength; i++) { + cursor = cropStart + i; + if (!hasGroupedData) { + if (data[cursor]) { + point = data[cursor]; + } else if (dataOptions[cursor] !== UNDEFINED) { // #970 + data[cursor] = point = (new pointClass()).init(series, dataOptions[cursor], processedXData[i]); + } + points[i] = point; + } else { + // splat the y data in case of ohlc data array + points[i] = (new pointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i]))); + points[i].dataGroup = series.groupMap[i]; + } + points[i].index = cursor; // For faster access in Point.update + } + + // Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when + // swithching view from non-grouped data to grouped data (#637) + if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) { + for (i = 0; i < dataLength; i++) { + if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points + i += processedDataLength; + } + if (data[i]) { + data[i].destroyElements(); + data[i].plotX = UNDEFINED; // #1003 + } + } + } + + series.data = data; + series.points = points; + }, + + /** + * Calculate Y extremes for visible data + */ + getExtremes: function (yData) { + var xAxis = this.xAxis, + yAxis = this.yAxis, + xData = this.processedXData, + yDataLength, + activeYData = [], + activeCounter = 0, + xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis + xMin = xExtremes.min, + xMax = xExtremes.max, + validValue, + withinRange, + x, + y, + i, + j; + + yData = yData || this.stackedYData || this.processedYData || []; + yDataLength = yData.length; + + for (i = 0; i < yDataLength; i++) { + + x = xData[i]; + y = yData[i]; + + // For points within the visible range, including the first point outside the + // visible range, consider y extremes + validValue = y !== null && y !== UNDEFINED && (!yAxis.isLog || (y.length || y > 0)); + withinRange = this.getExtremesFromAll || this.options.getExtremesFromAll || this.cropped || + ((xData[i + 1] || x) >= xMin && (xData[i - 1] || x) <= xMax); + + if (validValue && withinRange) { + + j = y.length; + if (j) { // array, like ohlc or range data + while (j--) { + if (y[j] !== null) { + activeYData[activeCounter++] = y[j]; + } + } + } else { + activeYData[activeCounter++] = y; + } + } + } + this.dataMin = arrayMin(activeYData); + this.dataMax = arrayMax(activeYData); + }, + + /** + * Translate data points from raw data values to chart specific positioning data + * needed later in drawPoints, drawGraph and drawTracker. + */ + translate: function () { + if (!this.processedXData) { // hidden series + this.processData(); + } + this.generatePoints(); + var series = this, + options = series.options, + stacking = options.stacking, + xAxis = series.xAxis, + categories = xAxis.categories, + yAxis = series.yAxis, + points = series.points, + dataLength = points.length, + hasModifyValue = !!series.modifyValue, + i, + pointPlacement = options.pointPlacement, + dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement), + threshold = options.threshold, + stackThreshold = options.startFromThreshold ? threshold : 0, + plotX, + plotY, + lastPlotX, + stackIndicator, + closestPointRangePx = Number.MAX_VALUE; + + // Translate each point + for (i = 0; i < dataLength; i++) { + var point = points[i], + xValue = point.x, + yValue = point.y, + yBottom = point.low, + stack = stacking && yAxis.stacks[(series.negStacks && yValue < (stackThreshold ? 0 : threshold) ? '-' : '') + series.stackKey], + pointStack, + stackValues; + + // Discard disallowed y values for log axes (#3434) + if (yAxis.isLog && yValue !== null && yValue <= 0) { + point.y = yValue = null; + error(10); + } + + // Get the plotX translation + point.plotX = plotX = correctFloat( // #5236 + mathMin(mathMax(-1e5, xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags')), 1e5) // #3923 + ); + + // Calculate the bottom y value for stacked series + if (stacking && series.visible && !point.isNull && stack && stack[xValue]) { + stackIndicator = series.getStackIndicator(stackIndicator, xValue, series.index); + pointStack = stack[xValue]; + stackValues = pointStack.points[stackIndicator.key]; + yBottom = stackValues[0]; + yValue = stackValues[1]; + + if (yBottom === stackThreshold && stackIndicator.key === stack[xValue].base) { + yBottom = pick(threshold, yAxis.min); + } + if (yAxis.isLog && yBottom <= 0) { // #1200, #1232 + yBottom = null; + } + + point.total = point.stackTotal = pointStack.total; + point.percentage = pointStack.total && (point.y / pointStack.total * 100); + point.stackY = yValue; + + // Place the stack label + pointStack.setOffset(series.pointXOffset || 0, series.barW || 0); + + } + + // Set translated yBottom or remove it + point.yBottom = defined(yBottom) ? + yAxis.translate(yBottom, 0, 1, 0, 1) : + null; + + // general hook, used for Highstock compare mode + if (hasModifyValue) { + yValue = series.modifyValue(yValue, point); + } + + // Set the the plotY value, reset it for redraws + point.plotY = plotY = (typeof yValue === 'number' && yValue !== Infinity) ? + mathMin(mathMax(-1e5, yAxis.translate(yValue, 0, 1, 0, 1)), 1e5) : // #3201 + UNDEFINED; + point.isInside = plotY !== UNDEFINED && plotY >= 0 && plotY <= yAxis.len && // #3519 + plotX >= 0 && plotX <= xAxis.len; + + + // Set client related positions for mouse tracking + point.clientX = dynamicallyPlaced ? xAxis.translate(xValue, 0, 0, 0, 1) : plotX; // #1514 + + point.negative = point.y < (threshold || 0); + + // some API data + point.category = categories && categories[point.x] !== UNDEFINED ? + categories[point.x] : point.x; + + // Determine auto enabling of markers (#3635, #5099) + if (!point.isNull) { + if (lastPlotX !== undefined) { + closestPointRangePx = mathMin(closestPointRangePx, mathAbs(plotX - lastPlotX)); + } + lastPlotX = plotX; + } + + } + series.closestPointRangePx = closestPointRangePx; + }, + + /** + * Return the series points with null points filtered out + */ + getValidPoints: function (points, insideOnly) { + var chart = this.chart; + return grep(points || this.points || [], function isValidPoint(point) { // #3916, #5029 + if (insideOnly && !chart.isInsidePlot(point.plotX, point.plotY, chart.inverted)) { // #5085 + return false; + } + return !point.isNull; + }); + }, + + /** + * Set the clipping for the series. For animated series it is called twice, first to initiate + * animating the clip then the second time without the animation to set the final clip. + */ + setClip: function (animation) { + var chart = this.chart, + options = this.options, + renderer = chart.renderer, + inverted = chart.inverted, + seriesClipBox = this.clipBox, + clipBox = seriesClipBox || chart.clipBox, + sharedClipKey = this.sharedClipKey || ['_sharedClip', animation && animation.duration, animation && animation.easing, clipBox.height, options.xAxis, options.yAxis].join(','), // #4526 + clipRect = chart[sharedClipKey], + markerClipRect = chart[sharedClipKey + 'm']; + + // If a clipping rectangle with the same properties is currently present in the chart, use that. + if (!clipRect) { + + // When animation is set, prepare the initial positions + if (animation) { + clipBox.width = 0; + + chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect( + -99, // include the width of the first marker + inverted ? -chart.plotLeft : -chart.plotTop, + 99, + inverted ? chart.chartWidth : chart.chartHeight + ); + } + chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox); + + } + if (animation) { + clipRect.count += 1; + } + + if (options.clip !== false) { + this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect); + this.markerGroup.clip(markerClipRect); + this.sharedClipKey = sharedClipKey; + } + + // Remove the shared clipping rectangle when all series are shown + if (!animation) { + clipRect.count -= 1; + if (clipRect.count <= 0 && sharedClipKey && chart[sharedClipKey]) { + if (!seriesClipBox) { + chart[sharedClipKey] = chart[sharedClipKey].destroy(); + } + if (chart[sharedClipKey + 'm']) { + chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy(); + } + } + } + }, + + /** + * Animate in the series + */ + animate: function (init) { + var series = this, + chart = series.chart, + clipRect, + animation = series.options.animation, + sharedClipKey; + + // Animation option is set to true + if (animation && !isObject(animation)) { + animation = defaultPlotOptions[series.type].animation; + } + + // Initialize the animation. Set up the clipping rectangle. + if (init) { + + series.setClip(animation); + + // Run the animation + } else { + sharedClipKey = this.sharedClipKey; + clipRect = chart[sharedClipKey]; + if (clipRect) { + clipRect.animate({ + width: chart.plotSizeX + }, animation); + } + if (chart[sharedClipKey + 'm']) { + chart[sharedClipKey + 'm'].animate({ + width: chart.plotSizeX + 99 + }, animation); + } + + // Delete this function to allow it only once + series.animate = null; + + } + }, + + /** + * This runs after animation to land on the final plot clipping + */ + afterAnimate: function () { + this.setClip(); + fireEvent(this, 'afterAnimate'); + }, + + /** + * Draw the markers + */ + drawPoints: function () { + var series = this, + pointAttr, + points = series.points, + chart = series.chart, + plotX, + plotY, + i, + point, + radius, + symbol, + isImage, + graphic, + options = series.options, + seriesMarkerOptions = options.marker, + seriesPointAttr = series.pointAttr[''], + pointMarkerOptions, + hasPointMarker, + enabled, + isInside, + markerGroup = series.markerGroup, + xAxis = series.xAxis, + globallyEnabled = pick( + seriesMarkerOptions.enabled, + xAxis.isRadial, + series.closestPointRangePx > 2 * seriesMarkerOptions.radius + ); + + if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) { + + i = points.length; + while (i--) { + point = points[i]; + plotX = mathFloor(point.plotX); // #1843 + plotY = point.plotY; + graphic = point.graphic; + pointMarkerOptions = point.marker || {}; + hasPointMarker = !!point.marker; + enabled = (globallyEnabled && pointMarkerOptions.enabled === UNDEFINED) || pointMarkerOptions.enabled; + isInside = point.isInside; + + // only draw the point if y is defined + if (enabled && isNumber(plotY) && point.y !== null) { + + // shortcuts + pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || seriesPointAttr; + radius = pointAttr.r; + symbol = pick(pointMarkerOptions.symbol, series.symbol); + isImage = symbol.indexOf('url') === 0; + + if (graphic) { // update + graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled + .attr(pointAttr) // #4759 + .animate(extend({ + x: plotX - radius, + y: plotY - radius + }, graphic.symbolName ? { // don't apply to image symbols #507 + width: 2 * radius, + height: 2 * radius + } : {})); + } else if (isInside && (radius > 0 || isImage)) { + point.graphic = graphic = chart.renderer.symbol( + symbol, + plotX - radius, + plotY - radius, + 2 * radius, + 2 * radius, + hasPointMarker ? pointMarkerOptions : seriesMarkerOptions + ) + .attr(pointAttr) + .add(markerGroup); + } + + } else if (graphic) { + point.graphic = graphic.destroy(); // #1269 + } + } + } + + }, + + /** + * 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, + seriesOptions = series.options, + normalOptions = defaultPlotOptions[series.type].marker ? seriesOptions.marker : seriesOptions, + stateOptions = normalOptions.states, + stateOptionsHover = stateOptions[HOVER_STATE], + pointStateOptionsHover, + seriesColor = series.color, + seriesNegativeColor = series.options.negativeColor, + normalDefaults = { + stroke: seriesColor, + fill: seriesColor + }, + points = series.points || [], // #927 + i, + j, + threshold, + point, + seriesPointAttr = [], + pointAttr, + pointAttrToOptions = series.pointAttrToOptions, + hasPointSpecificOptions = series.hasPointSpecificOptions, + defaultLineColor = normalOptions.lineColor, + defaultFillColor = normalOptions.fillColor, + turboThreshold = seriesOptions.turboThreshold, + zones = series.zones, + zoneAxis = series.zoneAxis || 'y', + zoneColor, + attr, + key; + + // series type specific modifications + if (seriesOptions.marker) { // line, spline, area, areaspline, scatter + + // if no hover radius is given, default to normal radius + 2 + stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + stateOptionsHover.radiusPlus; + stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + stateOptionsHover.lineWidthPlus; + + } 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(); + + // if no hover negativeColor is given, brighten the normal negativeColor + stateOptionsHover.negativeColor = stateOptionsHover.negativeColor || + Color(stateOptionsHover.negativeColor || seriesNegativeColor) + .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 = points.length; + if (!turboThreshold || i < turboThreshold || hasPointSpecificOptions) { + while (i--) { + point = points[i]; + normalOptions = (point.options && point.options.marker) || point.options; + if (normalOptions && normalOptions.enabled === false) { + normalOptions.radius = 0; + } + + zoneColor = null; + if (zones.length) { + j = 0; + threshold = zones[j]; + while (point[zoneAxis] >= threshold.value) { + threshold = zones[++j]; + } + + point.color = point.fillColor = zoneColor = pick(threshold.color, series.color); // #3636, #4267, #4430 - inherit color from series, when color is undefined + + } + + hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868 + + // 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) { + normalOptions = normalOptions || {}; + pointAttr = []; + stateOptions = normalOptions.states || {}; // reassign for individual point + pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {}; + + // Handle colors for column and pies + if (!seriesOptions.marker || (point.negative && !pointStateOptionsHover.fillColor && !stateOptionsHover.fillColor)) { // column, bar, point or negative threshold for series with markers (#3636) + // If no hover color is given, brighten the normal color. #1619, #2579 + pointStateOptionsHover[series.pointAttrToOptions.fill] = pointStateOptionsHover.color || (!point.options.color && stateOptionsHover[(point.negative && seriesNegativeColor ? 'negativeColor' : 'color')]) || + Color(point.color) + .brighten(pointStateOptionsHover.brightness || stateOptionsHover.brightness) + .get(); + } + + // normal point state inherits series wide normal state + attr = { color: point.color }; // #868 + if (!defaultFillColor) { // Individual point color or negative color markers (#2219) + attr.fillColor = point.color; + } + if (!defaultLineColor) { + attr.lineColor = point.color; // Bubbles take point color, line markers use white + } + // Color is explicitly set to null or undefined (#1288, #4068) + if (normalOptions.hasOwnProperty('color') && !normalOptions.color) { + delete normalOptions.color; + } + + // When zone is set, but series.states.hover.color is not set, apply zone color on hover, #4670: + if (zoneColor && !stateOptionsHover.fillColor) { + pointStateOptionsHover.fillColor = zoneColor; + } + + pointAttr[NORMAL_STATE] = series.convertAttribs(extend(attr, 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, + issue134 = /AppleWebKit\/533/.test(userAgent), + destroy, + i, + data = series.data || [], + point, + prop, + axis; + + // add event hook + fireEvent(series, 'destroy'); + + // remove all events + removeEvent(series); + + // erase from axes + each(series.axisTypes || [], function (AXIS) { + axis = series[AXIS]; + if (axis) { + erase(axis.series, series); + axis.isDirty = axis.forceRedraw = true; + } + }); + + // remove legend items + if (series.legendItem) { + series.chart.legend.destroyItem(series); + } + + // destroy all points with their elements + i = data.length; + while (i--) { + point = data[i]; + if (point && point.destroy) { + point.destroy(); + } + } + series.points = null; + + // Clear the animation timeout if we are destroying the series during initial animation + clearTimeout(series.animationTimeout); + + // Destroy all SVGElements associated to the series + for (prop in series) { + if (series[prop] instanceof SVGElement && !series[prop].survive) { // Survive provides a hook for not destroying + + // 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]; + } + }, + + /** + * Get the graph path + */ + getGraphPath: function (points, nullsAsZeroes, connectCliffs) { + var series = this, + options = series.options, + step = options.step, + reversed, + graphPath = [], + gap; + + points = points || series.points; + + // Bottom of a stack is reversed + reversed = points.reversed; + if (reversed) { + points.reverse(); + } + // Reverse the steps (#5004) + step = { right: 1, center: 2 }[step] || (step && 3); + if (step && reversed) { + step = 4 - step; + } + + // Remove invalid points, especially in spline (#5015) + if (options.connectNulls && !nullsAsZeroes && !connectCliffs) { + points = this.getValidPoints(points); + } + + // Build the line + each(points, function (point, i) { + + var plotX = point.plotX, + plotY = point.plotY, + lastPoint = points[i - 1], + pathToPoint; // the path to this point from the previous + + if ((point.leftCliff || (lastPoint && lastPoint.rightCliff)) && !connectCliffs) { + gap = true; // ... and continue + } + + // Line series, nullsAsZeroes is not handled + if (point.isNull && !defined(nullsAsZeroes) && i > 0) { + gap = !options.connectNulls; + + // Area series, nullsAsZeroes is set + } else if (point.isNull && !nullsAsZeroes) { + gap = true; + + } else { + + if (i === 0 || gap) { + pathToPoint = [M, point.plotX, point.plotY]; + + } else if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object + + pathToPoint = series.getPointSpline(points, point, i); + + } else if (step) { + + if (step === 1) { // right + pathToPoint = [ + L, + lastPoint.plotX, + plotY + ]; + + } else if (step === 2) { // center + pathToPoint = [ + L, + (lastPoint.plotX + plotX) / 2, + lastPoint.plotY, + L, + (lastPoint.plotX + plotX) / 2, + plotY + ]; + + } else { + pathToPoint = [ + L, + plotX, + lastPoint.plotY + ]; + } + pathToPoint.push(L, plotX, plotY); + + } else { + // normal line to next point + pathToPoint = [ + L, + plotX, + plotY + ]; + } + + + graphPath.push.apply(graphPath, pathToPoint); + gap = false; + } + }); + + series.graphPath = graphPath; + + return graphPath; + + }, + + /** + * Draw the actual graph + */ + drawGraph: function () { + var series = this, + options = this.options, + props = [['graph', options.lineColor || this.color, options.dashStyle]], + lineWidth = options.lineWidth, + roundCap = options.linecap !== 'square', + graphPath = (this.gappedPath || this.getGraphPath).call(this), + zones = this.zones; + + each(zones, function (threshold, i) { + props.push(['zoneGraph' + i, threshold.color || series.color, threshold.dashStyle || options.dashStyle]); + }); + + // Draw the graph + each(props, function (prop, i) { + var graphKey = prop[0], + graph = series[graphKey], + attribs; + + if (graph) { + graph.animate({ d: graphPath }); + + } else if (lineWidth && graphPath.length) { // #1487 + attribs = { + stroke: prop[1], + 'stroke-width': lineWidth, + fill: 'none', + zIndex: 1 // #1069 + }; + if (prop[2]) { + attribs.dashstyle = prop[2]; + } else if (roundCap) { + attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round'; + } + + series[graphKey] = series.chart.renderer.path(graphPath) + .attr(attribs) + .add(series.group) + .shadow((i < 2) && options.shadow); // add shadow to normal series (0) or to first zone (1) #3932 + } + }); + }, + + /** + * Clip the graphs into the positive and negative coloured graphs + */ + applyZones: function () { + var series = this, + chart = this.chart, + renderer = chart.renderer, + zones = this.zones, + translatedFrom, + translatedTo, + clips = this.clips || [], + clipAttr, + graph = this.graph, + area = this.area, + chartSizeMax = mathMax(chart.chartWidth, chart.chartHeight), + axis = this[(this.zoneAxis || 'y') + 'Axis'], + extremes, + reversed = axis.reversed, + inverted = chart.inverted, + horiz = axis.horiz, + pxRange, + pxPosMin, + pxPosMax, + ignoreZones = false; + + if (zones.length && (graph || area) && axis.min !== UNDEFINED) { + // The use of the Color Threshold assumes there are no gaps + // so it is safe to hide the original graph and area + if (graph) { + graph.hide(); + } + if (area) { + area.hide(); + } + + // Create the clips + extremes = axis.getExtremes(); + each(zones, function (threshold, i) { + + translatedFrom = reversed ? + (horiz ? chart.plotWidth : 0) : + (horiz ? 0 : axis.toPixels(extremes.min)); + translatedFrom = mathMin(mathMax(pick(translatedTo, translatedFrom), 0), chartSizeMax); + translatedTo = mathMin(mathMax(mathRound(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax); + + if (ignoreZones) { + translatedFrom = translatedTo = axis.toPixels(extremes.max); + } + + pxRange = Math.abs(translatedFrom - translatedTo); + pxPosMin = mathMin(translatedFrom, translatedTo); + pxPosMax = mathMax(translatedFrom, translatedTo); + if (axis.isXAxis) { + clipAttr = { + x: inverted ? pxPosMax : pxPosMin, + y: 0, + width: pxRange, + height: chartSizeMax + }; + if (!horiz) { + clipAttr.x = chart.plotHeight - clipAttr.x; + } + } else { + clipAttr = { + x: 0, + y: inverted ? pxPosMax : pxPosMin, + width: chartSizeMax, + height: pxRange + }; + if (horiz) { + clipAttr.y = chart.plotWidth - clipAttr.y; + } + } + + /// VML SUPPPORT + if (chart.inverted && renderer.isVML) { + if (axis.isXAxis) { + clipAttr = { + x: 0, + y: reversed ? pxPosMin : pxPosMax, + height: clipAttr.width, + width: chart.chartWidth + }; + } else { + clipAttr = { + x: clipAttr.y - chart.plotLeft - chart.spacingBox.x, + y: 0, + width: clipAttr.height, + height: chart.chartHeight + }; + } + } + /// END OF VML SUPPORT + + if (clips[i]) { + clips[i].animate(clipAttr); + } else { + clips[i] = renderer.clipRect(clipAttr); + + if (graph) { + series['zoneGraph' + i].clip(clips[i]); + } + + if (area) { + series['zoneArea' + i].clip(clips[i]); + } + } + // if this zone extends out of the axis, ignore the others + ignoreZones = threshold.value > extremes.max; + }); + this.clips = clips; + } + }, + + /** + * Initialize and perform group inversion on series.group and series.markerGroup + */ + invertGroups: function () { + var series = this, + chart = series.chart; + + // Pie, go away (#1736) + if (!series.xAxis) { + return; + } + + // A fixed size is needed for inversion to work + function setInvert() { + var size = { + width: series.yAxis.len, + height: series.xAxis.len + }; + + each(['group', 'markerGroup'], function (groupName) { + if (series[groupName]) { + series[groupName].attr(size).invert(); + } + }); + } + + addEvent(chart, 'resize', setInvert); // do it on resize + addEvent(series, 'destroy', function () { + removeEvent(chart, 'resize', setInvert); + }); + + // Do it now + setInvert(); // do it now + + // On subsequent render and redraw, just do setInvert without setting up events again + series.invertGroups = setInvert; + }, + + /** + * General abstraction for creating plot groups like series.group, series.dataLabelsGroup and + * series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size. + */ + plotGroup: function (prop, name, visibility, zIndex, parent) { + var group = this[prop], + isNew = !group; + + // Generate it on first call + if (isNew) { + this[prop] = group = this.chart.renderer.g(name) + .attr({ + zIndex: zIndex || 0.1 // IE8 and pointer logic use this + }) + .add(parent); + + group.addClass('highcharts-series-' + this.index); + } + + // Place it on first and subsequent (redraw) calls + group.attr({ visibility: visibility })[isNew ? 'attr' : 'animate'](this.getPlotBox()); + return group; + }, + + /** + * Get the translation and scale for the plot area of this series + */ + getPlotBox: function () { + var chart = this.chart, + xAxis = this.xAxis, + yAxis = this.yAxis; + + // Swap axes for inverted (#2339) + if (chart.inverted) { + xAxis = yAxis; + yAxis = this.xAxis; + } + return { + translateX: xAxis ? xAxis.left : chart.plotLeft, + translateY: yAxis ? yAxis.top : chart.plotTop, + scaleX: 1, // #1623 + scaleY: 1 + }; + }, + + /** + * Render the graph and markers + */ + render: function () { + var series = this, + chart = series.chart, + group, + options = series.options, + // Animation doesn't work in IE8 quirks when the group div is hidden, + // and looks bad in other oldIE + animDuration = !!series.animate && chart.renderer.isSVG && animObject(options.animation).duration, + visibility = series.visible ? 'inherit' : 'hidden', // #2597 + zIndex = options.zIndex, + hasRendered = series.hasRendered, + chartSeriesGroup = chart.seriesGroup; + + // the group + group = series.plotGroup( + 'group', + 'series', + visibility, + zIndex, + chartSeriesGroup + ); + + series.markerGroup = series.plotGroup( + 'markerGroup', + 'markers', + visibility, + zIndex, + chartSeriesGroup + ); + + // initiate the animation + if (animDuration) { + series.animate(true); + } + + // cache attributes for shapes + series.getAttribs(); + + // SVGRenderer needs to know this before drawing elements (#1089, #1795) + group.inverted = series.isCartesian ? chart.inverted : false; + + // draw the graph if any + if (series.drawGraph) { + series.drawGraph(); + series.applyZones(); + } + + each(series.points, function (point) { + if (point.redraw) { + point.redraw(); + } + }); + + // draw the data labels (inn pies they go before the points) + if (series.drawDataLabels) { + series.drawDataLabels(); + } + + // draw the points + if (series.visible) { + series.drawPoints(); + } + + + // draw the mouse tracking area + if (series.drawTracker && series.options.enableMouseTracking !== false) { + series.drawTracker(); + } + + // Handle inverted series and tracker groups + if (chart.inverted) { + series.invertGroups(); + } + + // Initial clipping, must be defined after inverting groups for VML. Applies to columns etc. (#3839). + if (options.clip !== false && !series.sharedClipKey && !hasRendered) { + group.clip(chart.clipRect); + } + + // Run the animation + if (animDuration) { + series.animate(); + } + + // Call the afterAnimate function on animation complete (but don't overwrite the animation.complete option + // which should be available to the user). + if (!hasRendered) { + series.animationTimeout = syncTimeout(function () { + series.afterAnimate(); + }, animDuration); + } + + series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see + // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see + series.hasRendered = true; + }, + + /** + * Redraw the series after an update in the axes. + */ + redraw: function () { + var series = this, + chart = series.chart, + wasDirty = series.isDirty || series.isDirtyData, // cache it here as it is set to false in render, but used after + group = series.group, + xAxis = series.xAxis, + yAxis = series.yAxis; + + // reposition on resize + if (group) { + if (chart.inverted) { + group.attr({ + width: chart.plotWidth, + height: chart.plotHeight + }); + } + + group.animate({ + translateX: pick(xAxis && xAxis.left, chart.plotLeft), + translateY: pick(yAxis && yAxis.top, chart.plotTop) + }); + } + + series.translate(); + series.render(); + if (wasDirty) { // #3868, #3945 + delete this.kdTree; + } + }, + + /** + * KD Tree && PointSearching Implementation + */ + + kdDimensions: 1, + kdAxisArray: ['clientX', 'plotY'], + + searchPoint: function (e, compareX) { + var series = this, + xAxis = series.xAxis, + yAxis = series.yAxis, + inverted = series.chart.inverted; + + return this.searchKDTree({ + clientX: inverted ? xAxis.len - e.chartY + xAxis.pos : e.chartX - xAxis.pos, + plotY: inverted ? yAxis.len - e.chartX + yAxis.pos : e.chartY - yAxis.pos + }, compareX); + }, + + buildKDTree: function () { + var series = this, + dimensions = series.kdDimensions; + + // Internal function + function _kdtree(points, depth, dimensions) { + var axis, + median, + length = points && points.length; + + if (length) { + + // alternate between the axis + axis = series.kdAxisArray[depth % dimensions]; + + // sort point array + points.sort(function (a, b) { + return a[axis] - b[axis]; + }); + + median = Math.floor(length / 2); + + // build and return nod + return { + point: points[median], + left: _kdtree(points.slice(0, median), depth + 1, dimensions), + right: _kdtree(points.slice(median + 1), depth + 1, dimensions) + }; + + } + } + + // Start the recursive build process with a clone of the points array and null points filtered out (#3873) + function startRecursive() { + series.kdTree = _kdtree( + series.getValidPoints( + null, + !series.directTouch // For line-type series restrict to plot area, but column-type series not (#3916, #4511) + ), + dimensions, + dimensions + ); + } + delete series.kdTree; + + // For testing tooltips, don't build async + syncTimeout(startRecursive, series.options.kdNow ? 0 : 1); + }, + + searchKDTree: function (point, compareX) { + var series = this, + kdX = this.kdAxisArray[0], + kdY = this.kdAxisArray[1], + kdComparer = compareX ? 'distX' : 'dist'; + + // Set the one and two dimensional distance on the point object + function setDistance(p1, p2) { + var x = (defined(p1[kdX]) && defined(p2[kdX])) ? Math.pow(p1[kdX] - p2[kdX], 2) : null, + y = (defined(p1[kdY]) && defined(p2[kdY])) ? Math.pow(p1[kdY] - p2[kdY], 2) : null, + r = (x || 0) + (y || 0); + + p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE; + p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE; + } + function _search(search, tree, depth, dimensions) { + var point = tree.point, + axis = series.kdAxisArray[depth % dimensions], + tdist, + sideA, + sideB, + ret = point, + nPoint1, + nPoint2; + + setDistance(search, point); + + // Pick side based on distance to splitting point + tdist = search[axis] - point[axis]; + sideA = tdist < 0 ? 'left' : 'right'; + sideB = tdist < 0 ? 'right' : 'left'; + + // End of tree + if (tree[sideA]) { + nPoint1 = _search(search, tree[sideA], depth + 1, dimensions); + + ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point); + } + if (tree[sideB]) { + // compare distance to current best to splitting point to decide wether to check side B or not + if (Math.sqrt(tdist * tdist) < ret[kdComparer]) { + nPoint2 = _search(search, tree[sideB], depth + 1, dimensions); + ret = (nPoint2[kdComparer] < ret[kdComparer] ? nPoint2 : ret); + } + } + + return ret; + } + + if (!this.kdTree) { + this.buildKDTree(); + } + + if (this.kdTree) { + return _search(point, + this.kdTree, this.kdDimensions, this.kdDimensions); + } + } + + }; // end Series prototype + + /** + * The class for stack items + */ + function StackItem(axis, options, isNegative, x, stackOption) { + + var inverted = axis.chart.inverted; + + this.axis = axis; + + // Tells if the stack is negative + this.isNegative = isNegative; + + // Save the options to be able to style the label + this.options = options; + + // Save the x value to be able to position the label later + this.x = x; + + // Initialize total value + this.total = null; + + // This will keep each points' extremes stored by series.index and point index + this.points = {}; + + // Save the stack option on the series configuration object, and whether to treat it as percent + this.stack = stackOption; + this.leftCliff = 0; + this.rightCliff = 0; + + // 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. + this.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) + }; + + this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center'); + } + + StackItem.prototype = { + destroy: function () { + destroyObjectProperties(this, this.axis); + }, + + /** + * Renders the stack total label and adds it to the stack label group. + */ + render: function (group) { + var options = this.options, + formatOption = options.format, + str = formatOption ? + format(formatOption, this) : + options.formatter.call(this); // 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 (this.label) { + this.label.attr({ text: str, visibility: 'hidden' }); + // Create new label + } else { + this.label = + this.axis.chart.renderer.text(str, null, null, options.useHTML) // dummy positions, actual position updated with setOffset method in columnseries + .css(options.style) // apply style + .attr({ + align: this.textAlign, // fix the text-anchor + rotation: 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, + axis = stackItem.axis, + chart = axis.chart, + inverted = chart.inverted, + reversed = axis.reversed, + neg = (this.isNegative && !reversed) || (!this.isNegative && reversed), // #4056 + y = axis.translate(axis.usePercentage ? 100 : this.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates + yZero = axis.translate(0), // stack origin + h = mathAbs(y - yZero), // stack height + x = chart.xAxis[0].translate(this.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 + }, + label = this.label, + alignAttr; + + if (label) { + label.align(this.alignOptions, null, stackBox); // align the label to the box + + // Set visibility (#678) + alignAttr = label.alignAttr; + label[this.options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) ? 'show' : 'hide'](true); + } + } + }; + + /** + * Generate stacks for each series and calculate stacks total values + */ + Chart.prototype.getStacks = function () { + var chart = this; + + // reset stacks for each yAxis + each(chart.yAxis, function (axis) { + if (axis.stacks && axis.hasVisibleSeries) { + axis.oldStacks = axis.stacks; + } + }); + + each(chart.series, function (series) { + if (series.options.stacking && (series.visible === true || chart.options.chart.ignoreHiddenSeries === false)) { + series.stackKey = series.type + pick(series.options.stack, ''); + } + }); + }; + + + // Stacking methods defined on the Axis prototype + + /** + * Build the stacks from top down + */ + Axis.prototype.buildStacks = function () { + var axisSeries = this.series, + series, + reversedStacks = pick(this.options.reversedStacks, true), + len = axisSeries.length, + i; + if (!this.isXAxis) { + this.usePercentage = false; + i = len; + while (i--) { + axisSeries[reversedStacks ? i : len - i - 1].setStackedPoints(); + } + + i = len; + while (i--) { + series = axisSeries[reversedStacks ? i : len - i - 1]; + if (series.setStackCliffs) { + series.setStackCliffs(); + } + } + // Loop up again to compute percent stack + if (this.usePercentage) { + for (i = 0; i < len; i++) { + axisSeries[i].setPercentStacks(); + } + } + } + }; + + Axis.prototype.renderStackTotals = function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + stacks = axis.stacks, + 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 + }) + .add(); + } + + // plotLeft/Top will change when y axis gets wider so we need to translate the + // stackTotalGroup at every render call. See bug #506 and #516 + stackTotalGroup.translate(chart.plotLeft, chart.plotTop); + + // Render each stack total + for (stackKey in stacks) { + oneStack = stacks[stackKey]; + for (stackCategory in oneStack) { + oneStack[stackCategory].render(stackTotalGroup); + } + } + }; + + /** + * Set all the stacks to initial states and destroy unused ones. + */ + Axis.prototype.resetStacks = function () { + var stacks = this.stacks, + type, + i; + if (!this.isXAxis) { + for (type in stacks) { + for (i in stacks[type]) { + + // Clean up memory after point deletion (#1044, #4320) + if (stacks[type][i].touched < this.stacksTouched) { + stacks[type][i].destroy(); + delete stacks[type][i]; + + // Reset stacks + } else { + stacks[type][i].total = null; + stacks[type][i].cum = 0; + } + } + } + } + }; + + Axis.prototype.cleanStacks = function () { + var stacks, type, i; + + if (!this.isXAxis) { + if (this.oldStacks) { + stacks = this.stacks = this.oldStacks; + } + + // reset stacks + for (type in stacks) { + for (i in stacks[type]) { + stacks[type][i].cum = stacks[type][i].total; + } + } + } + }; + + + // Stacking methods defnied for Series prototype + + /** + * Adds series' points value to corresponding stack + */ + Series.prototype.setStackedPoints = function () { + if (!this.options.stacking || (this.visible !== true && this.chart.options.chart.ignoreHiddenSeries !== false)) { + return; + } + + var series = this, + xData = series.processedXData, + yData = series.processedYData, + stackedYData = [], + yDataLength = yData.length, + seriesOptions = series.options, + threshold = seriesOptions.threshold, + stackThreshold = seriesOptions.startFromThreshold ? threshold : 0, + stackOption = seriesOptions.stack, + stacking = seriesOptions.stacking, + stackKey = series.stackKey, + negKey = '-' + stackKey, + negStacks = series.negStacks, + yAxis = series.yAxis, + stacks = yAxis.stacks, + oldStacks = yAxis.oldStacks, + stackIndicator, + isNegative, + stack, + other, + key, + pointKey, + i, + x, + y; + + + yAxis.stacksTouched += 1; + + // loop over the non-null y values and read them into a local array + for (i = 0; i < yDataLength; i++) { + x = xData[i]; + y = yData[i]; + stackIndicator = series.getStackIndicator(stackIndicator, x, series.index); + pointKey = stackIndicator.key; + // Read stacked values into a stack based on the x value, + // the sign of y and the stack key. Stacking is also handled for null values (#739) + isNegative = negStacks && y < (stackThreshold ? 0 : threshold); + key = isNegative ? negKey : stackKey; + + // Create empty object for this stack if it doesn't exist yet + if (!stacks[key]) { + stacks[key] = {}; + } + + // Initialize StackItem for this x + if (!stacks[key][x]) { + if (oldStacks[key] && oldStacks[key][x]) { + stacks[key][x] = oldStacks[key][x]; + stacks[key][x].total = null; + } else { + stacks[key][x] = new StackItem(yAxis, yAxis.options.stackLabels, isNegative, x, stackOption); + } + } + + // If the StackItem doesn't exist, create it first + stack = stacks[key][x]; + if (y !== null) { + stack.points[pointKey] = stack.points[series.index] = [pick(stack.cum, stackThreshold)]; + + // Record the base of the stack + if (!defined(stack.cum)) { + stack.base = pointKey; + } + stack.touched = yAxis.stacksTouched; + + + // In area charts, if there are multiple points on the same X value, let the + // area fill the full span of those points + if (stackIndicator.index > 0 && series.singleStacks === false) { + stack.points[pointKey][0] = stack.points[series.index + ',' + x + ',0'][0]; + } + } + + // Add value to the stack total + if (stacking === 'percent') { + + // Percent stacked column, totals are the same for the positive and negative stacks + other = isNegative ? stackKey : negKey; + if (negStacks && stacks[other] && stacks[other][x]) { + other = stacks[other][x]; + stack.total = other.total = mathMax(other.total, stack.total) + mathAbs(y) || 0; + + // Percent stacked areas + } else { + stack.total = correctFloat(stack.total + (mathAbs(y) || 0)); + } + } else { + stack.total = correctFloat(stack.total + (y || 0)); + } + + stack.cum = pick(stack.cum, stackThreshold) + (y || 0); + + if (y !== null) { + stack.points[pointKey].push(stack.cum); + stackedYData[i] = stack.cum; + } + + } + + if (stacking === 'percent') { + yAxis.usePercentage = true; + } + + this.stackedYData = stackedYData; // To be used in getExtremes + + // Reset old stacks + yAxis.oldStacks = {}; + }; + + /** + * Iterate over all stacks and compute the absolute values to percent + */ + Series.prototype.setPercentStacks = function () { + var series = this, + stackKey = series.stackKey, + stacks = series.yAxis.stacks, + processedXData = series.processedXData, + stackIndicator; + + each([stackKey, '-' + stackKey], function (key) { + var i = processedXData.length, + x, + stack, + pointExtremes, + totalFactor; + + while (i--) { + x = processedXData[i]; + stackIndicator = series.getStackIndicator(stackIndicator, x, series.index); + stack = stacks[key] && stacks[key][x]; + pointExtremes = stack && stack.points[stackIndicator.key]; + if (pointExtremes) { + totalFactor = stack.total ? 100 / stack.total : 0; + pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); // Y bottom value + pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); // Y value + series.stackedYData[i] = pointExtremes[1]; + } + } + }); + }; + + /** + * Get stack indicator, according to it's x-value, to determine points with the same x-value + */ + Series.prototype.getStackIndicator = function (stackIndicator, x, index) { + if (!defined(stackIndicator) || stackIndicator.x !== x) { + stackIndicator = { + x: x, + index: 0 + }; + } else { + stackIndicator.index++; + } + + stackIndicator.key = [index, x, stackIndicator.index].join(','); + + return stackIndicator; + }; + + // Extend the Chart prototype for dynamic methods + extend(Chart.prototype, { + + /** + * 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 + */ + addSeries: function (options, redraw, animation) { + var series, + chart = this; + + if (options) { + redraw = pick(redraw, true); // defaults to true + + fireEvent(chart, 'addSeries', { options: options }, function () { + series = chart.initSeries(options); + + chart.isDirtyLegend = true; // the series array is out of sync with the display + chart.linkSeries(); + if (redraw) { + chart.redraw(animation); + } + }); + } + + return series; + }, + + /** + * Add an axis to the chart + * @param {Object} options The axis option + * @param {Boolean} isX Whether it is an X axis or a value axis + */ + addAxis: function (options, isX, redraw, animation) { + var key = isX ? 'xAxis' : 'yAxis', + chartOptions = this.options, + userOptions = merge(options, { + index: this[key].length, + isX: isX + }); + + new Axis(this, userOptions); // eslint-disable-line no-new + + // Push the new axis options to the chart options + chartOptions[key] = splat(chartOptions[key] || {}); + chartOptions[key].push(userOptions); + + if (pick(redraw, true)) { + this.redraw(animation); + } + }, + + /** + * 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 + */ + showLoading: function (str) { + var chart = this, + options = chart.options, + loadingDiv = chart.loadingDiv, + loadingOptions = options.loading, + setLoadingSize = function () { + if (loadingDiv) { + css(loadingDiv, { + left: chart.plotLeft + PX, + top: chart.plotTop + PX, + width: chart.plotWidth + PX, + height: chart.plotHeight + PX + }); + } + }; + + // create the layer at the first call + if (!loadingDiv) { + chart.loadingDiv = loadingDiv = createElement(DIV, { + className: PREFIX + 'loading' + }, extend(loadingOptions.style, { + zIndex: 10, + display: NONE + }), chart.container); + + chart.loadingSpan = createElement( + 'span', + null, + loadingOptions.labelStyle, + loadingDiv + ); + addEvent(chart, 'redraw', setLoadingSize); // #1080 + } + + // update text + chart.loadingSpan.innerHTML = str || options.lang.loading; + + // show it + if (!chart.loadingShown) { + css(loadingDiv, { + opacity: 0, + display: '' + }); + animate(loadingDiv, { + opacity: loadingOptions.style.opacity + }, { + duration: loadingOptions.showDuration || 0 + }); + chart.loadingShown = true; + } + setLoadingSize(); + }, + + /** + * Hide the loading layer + */ + hideLoading: function () { + var options = this.options, + loadingDiv = this.loadingDiv; + + if (loadingDiv) { + animate(loadingDiv, { + opacity: 0 + }, { + duration: options.loading.hideDuration || 100, + complete: function () { + css(loadingDiv, { display: NONE }); + } + }); + } + this.loadingShown = false; + } + }); + + // extend the Point prototype for dynamic methods + extend(Point.prototype, { + /** + * 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, runEvent) { + var point = this, + series = point.series, + graphic = point.graphic, + i, + chart = series.chart, + seriesOptions = series.options, + names = series.xAxis && series.xAxis.names; + + redraw = pick(redraw, true); + + function update() { + + point.applyOptions(options); + + // Update visuals + if (point.y === null && graphic) { // #4146 + point.graphic = graphic.destroy(); + } + if (isObject(options) && !isArray(options)) { + // Defer the actual redraw until getAttribs has been called (#3260) + point.redraw = function () { + if (graphic && graphic.element) { + if (options && options.marker && options.marker.symbol) { + point.graphic = graphic.destroy(); + } + } + if (options && options.dataLabels && point.dataLabel) { // #2468 + point.dataLabel = point.dataLabel.destroy(); + } + point.redraw = null; + }; + } + + // record changes in the parallel arrays + i = point.index; + series.updateParallelArrays(point, i); + if (names && point.name) { + names[point.x] = point.name; + } + + // Record the options to options.data. If there is an object from before, + // use point options, otherwise use raw options. (#4701) + seriesOptions.data[i] = (isObject(seriesOptions.data[i]) && !isArray(seriesOptions.data[i])) ? point.options : options; + + // redraw + series.isDirty = series.isDirtyData = true; + if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320 + chart.isDirtyBox = true; + } + + if (seriesOptions.legendType === 'point') { // #1831, #1885 + chart.isDirtyLegend = true; + } + if (redraw) { + chart.redraw(animation); + } + } + + // Fire the event with a default handler of doing the update + if (runEvent === false) { // When called from setData + update(); + } else { + point.firePointEvent('update', { options: options }, update); + } + }, + + /** + * 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) { + this.series.removePoint(inArray(this, this.series.data), redraw, animation); + } + }); + + // Extend the series prototype for dynamic methods + extend(Series.prototype, { + /** + * 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, + seriesOptions = series.options, + data = series.data, + graph = series.graph, + area = series.area, + chart = series.chart, + names = series.xAxis && series.xAxis.names, + currentShift = (graph && graph.shift) || 0, + shiftShapes = ['graph', 'area'], + dataOptions = seriesOptions.data, + point, + isInTheMiddle, + xData = series.xData, + i, + x; + + setAnimation(animation, chart); + + // Make graph animate sideways + if (shift) { + i = series.zones.length; + while (i--) { + shiftShapes.push('zoneGraph' + i, 'zoneArea' + i); + } + each(shiftShapes, function (shape) { + if (series[shape]) { + series[shape].shift = currentShift + (seriesOptions.step ? 2 : 1); + } + }); + } + if (area) { + area.isArea = true; // needed in animation, both with and without shift + } + + // Optional redraw, defaults to true + redraw = pick(redraw, true); + + // Get options and push the point to xData, yData and series.options. In series.generatePoints + // the Point instance will be created on demand and pushed to the series.data array. + point = { series: series }; + series.pointClass.prototype.applyOptions.apply(point, [options]); + x = point.x; + + // Get the insertion point + i = xData.length; + if (series.requireSorting && x < xData[i - 1]) { + isInTheMiddle = true; + while (i && xData[i - 1] > x) { + i--; + } + } + + series.updateParallelArrays(point, 'splice', i, 0, 0); // insert undefined item + series.updateParallelArrays(point, i); // update it + + if (names && point.name) { + names[x] = point.name; + } + dataOptions.splice(i, 0, options); + + if (isInTheMiddle) { + series.data.splice(i, 0, null); + series.processData(); + } + + // Generate points to be added to the legend (#1329) + if (seriesOptions.legendType === 'point') { + series.generatePoints(); + } + + // Shift the first point off the parallel arrays + if (shift) { + if (data[0] && data[0].remove) { + data[0].remove(false); + } else { + data.shift(); + series.updateParallelArrays(point, 'shift'); + + dataOptions.shift(); + } + } + + // redraw + series.isDirty = true; + series.isDirtyData = true; + if (redraw) { + series.getAttribs(); // #1937 + chart.redraw(); + } + }, + + /** + * Remove a point (rendered or not), by index + */ + removePoint: function (i, redraw, animation) { + + var series = this, + data = series.data, + point = data[i], + points = series.points, + chart = series.chart, + remove = function () { + + if (points && points.length === data.length) { // #4935 + points.splice(i, 1); + } + data.splice(i, 1); + series.options.data.splice(i, 1); + series.updateParallelArrays(point || { series: series }, 'splice', i, 1); + + if (point) { + point.destroy(); + } + + // redraw + series.isDirty = true; + series.isDirtyData = true; + if (redraw) { + chart.redraw(); + } + }; + + setAnimation(animation, chart); + redraw = pick(redraw, true); + + // Fire the event with a default handler of removing the point + if (point) { + point.firePointEvent('remove', null, remove); + } else { + remove(); + } + }, + + /** + * 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; + + // 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; + chart.linkSeries(); + + if (pick(redraw, true)) { + chart.redraw(animation); + } + }); + }, + + /** + * Update the series with a new set of options + */ + update: function (newOptions, redraw) { + var series = this, + chart = this.chart, + // must use user options when changing type because this.options is merged + // in with type specific plotOptions + oldOptions = this.userOptions, + oldType = this.type, + proto = seriesTypes[oldType].prototype, + preserve = ['group', 'markerGroup', 'dataLabelsGroup'], + n; + + // If we're changing type or zIndex, create new groups (#3380, #3404) + if ((newOptions.type && newOptions.type !== oldType) || newOptions.zIndex !== undefined) { + preserve.length = 0; + } + + // Make sure groups are not destroyed (#3094) + each(preserve, function (prop) { + preserve[prop] = series[prop]; + delete series[prop]; + }); + + // Do the merge, with some forced options + newOptions = merge(oldOptions, { + animation: false, + index: this.index, + pointStart: this.xData[0] // when updating after addPoint + }, { data: this.options.data }, newOptions); + + // Destroy the series and delete all properties. Reinsert all methods + // and properties from the new type prototype (#2270, #3719) + this.remove(false); + for (n in proto) { + this[n] = UNDEFINED; + } + extend(this, seriesTypes[newOptions.type || oldType].prototype); + + // Re-register groups (#3094) + each(preserve, function (prop) { + series[prop] = preserve[prop]; + }); + + this.init(chart, newOptions); + chart.linkSeries(); // Links are lost in this.remove (#3028) + if (pick(redraw, true)) { + chart.redraw(false); + } + } + }); + + // Extend the Axis.prototype for dynamic methods + extend(Axis.prototype, { + + /** + * Update the axis with a new options structure + */ + update: function (newOptions, redraw) { + var chart = this.chart; + + newOptions = chart.options[this.coll][this.options.index] = merge(this.userOptions, newOptions); + + this.destroy(true); + + this.init(chart, extend(newOptions, { events: UNDEFINED })); + + chart.isDirtyBox = true; + if (pick(redraw, true)) { + chart.redraw(); + } + }, + + /** + * Remove the axis from the chart + */ + remove: function (redraw) { + var chart = this.chart, + key = this.coll, // xAxis or yAxis + axisSeries = this.series, + i = axisSeries.length; + + // Remove associated series (#2687) + while (i--) { + if (axisSeries[i]) { + axisSeries[i].remove(false); + } + } + + // Remove the axis + erase(chart.axes, this); + erase(chart[key], this); + chart.options[key].splice(this.options.index, 1); + each(chart[key], function (axis, i) { // Re-index, #1706 + axis.options.index = i; + }); + this.destroy(); + chart.isDirtyBox = true; + + if (pick(redraw, true)) { + chart.redraw(); + } + }, + + /** + * Update the axis title by options + */ + setTitle: function (newTitleOptions, redraw) { + this.update({ title: newTitleOptions }, redraw); + }, + + /** + * Set new axis categories and optionally redraw + * @param {Array} categories + * @param {Boolean} redraw + */ + setCategories: function (categories, redraw) { + this.update({ categories: categories }, redraw); + } + + }); + + + /** + * LineSeries object + */ + var LineSeries = extendClass(Series); + seriesTypes.line = LineSeries; + + /** + * Set the default options for area + */ + defaultPlotOptions.area = merge(defaultSeriesOptions, { + softThreshold: false, + threshold: 0 + // trackByArea: false, + // lineColor: null, // overrides color, but lets fillColor be unaltered + // fillOpacity: 0.75, + // fillColor: null + }); + + /** + * AreaSeries object + */ + var AreaSeries = extendClass(Series, { + type: 'area', + singleStacks: false, + /** + * Return an array of stacked points, where null and missing points are replaced by + * dummy points in order for gaps to be drawn correctly in stacks. + */ + getStackPoints: function () { + var series = this, + segment = [], + keys = [], + xAxis = this.xAxis, + yAxis = this.yAxis, + stack = yAxis.stacks[this.stackKey], + pointMap = {}, + points = this.points, + seriesIndex = series.index, + yAxisSeries = yAxis.series, + seriesLength = yAxisSeries.length, + visibleSeries, + upOrDown = pick(yAxis.options.reversedStacks, true) ? 1 : -1, + i, + x; + + if (this.options.stacking) { + // Create a map where we can quickly look up the points by their X value. + for (i = 0; i < points.length; i++) { + pointMap[points[i].x] = points[i]; + } + + // Sort the keys (#1651) + for (x in stack) { + if (stack[x].total !== null) { // nulled after switching between grouping and not (#1651, #2336) + keys.push(x); + } + } + keys.sort(function (a, b) { + return a - b; + }); + + visibleSeries = map(yAxisSeries, function () { + return this.visible; + }); + + each(keys, function (x, idx) { + var y = 0, + stackPoint, + stackedValues; + + if (pointMap[x] && !pointMap[x].isNull) { + segment.push(pointMap[x]); + + // Find left and right cliff. -1 goes left, 1 goes right. + each([-1, 1], function (direction) { + var nullName = direction === 1 ? 'rightNull' : 'leftNull', + cliffName = direction === 1 ? 'rightCliff' : 'leftCliff', + cliff = 0, + otherStack = stack[keys[idx + direction]]; + + // If there is a stack next to this one, to the left or to the right... + if (otherStack) { + i = seriesIndex; + while (i >= 0 && i < seriesLength) { // Can go either up or down, depending on reversedStacks + stackPoint = otherStack.points[i]; + if (!stackPoint) { + // If the next point in this series is missing, mark the point + // with point.leftNull or point.rightNull = true. + if (i === seriesIndex) { + pointMap[x][nullName] = true; + + // If there are missing points in the next stack in any of the + // series below this one, we need to substract the missing values + // and add a hiatus to the left or right. + } else if (visibleSeries[i]) { + stackedValues = stack[x].points[i]; + if (stackedValues) { + cliff -= stackedValues[1] - stackedValues[0]; + } + } + } + // When reversedStacks is true, loop up, else loop down + i += upOrDown; + } + } + pointMap[x][cliffName] = cliff; + }); + + + // There is no point for this X value in this series, so we + // insert a dummy point in order for the areas to be drawn + // correctly. + } else { + + // Loop down the stack to find the series below this one that has + // a value (#1991) + i = seriesIndex; + while (i >= 0 && i < seriesLength) { + stackPoint = stack[x].points[i]; + if (stackPoint) { + y = stackPoint[1]; + break; + } + // When reversedStacks is true, loop up, else loop down + i += upOrDown; + } + + y = yAxis.toPixels(y, true); + segment.push({ + isNull: true, + plotX: xAxis.toPixels(x, true), + plotY: y, + yBottom: y + }); + } + }); + + } + + return segment; + }, + + getGraphPath: function (points) { + var getGraphPath = Series.prototype.getGraphPath, + graphPath, + options = this.options, + stacking = options.stacking, + yAxis = this.yAxis, + topPath, + //topPoints = [], + bottomPath, + bottomPoints = [], + graphPoints = [], + seriesIndex = this.index, + i, + areaPath, + plotX, + stacks = yAxis.stacks[this.stackKey], + threshold = options.threshold, + translatedThreshold = yAxis.getThreshold(options.threshold), + isNull, + yBottom, + connectNulls = options.connectNulls || stacking === 'percent', + /** + * To display null points in underlying stacked series, this series graph must be + * broken, and the area also fall down to fill the gap left by the null point. #2069 + */ + addDummyPoints = function (i, otherI, side) { + var point = points[i], + stackedValues = stacking && stacks[point.x].points[seriesIndex], + nullVal = point[side + 'Null'] || 0, + cliffVal = point[side + 'Cliff'] || 0, + top, + bottom, + isNull = true; + + if (cliffVal || nullVal) { + + top = (nullVal ? stackedValues[0] : stackedValues[1]) + cliffVal; + bottom = stackedValues[0] + cliffVal; + isNull = !!nullVal; + + } else if (!stacking && points[otherI] && points[otherI].isNull) { + top = bottom = threshold; + } + + // Add to the top and bottom line of the area + if (top !== undefined) { + graphPoints.push({ + plotX: plotX, + plotY: top === null ? translatedThreshold : yAxis.getThreshold(top), + isNull: isNull + }); + bottomPoints.push({ + plotX: plotX, + plotY: bottom === null ? translatedThreshold : yAxis.getThreshold(bottom) + }); + } + }; + + // Find what points to use + points = points || this.points; + + + // Fill in missing points + if (stacking) { + points = this.getStackPoints(); + } + + for (i = 0; i < points.length; i++) { + isNull = points[i].isNull; + plotX = pick(points[i].rectPlotX, points[i].plotX); + yBottom = pick(points[i].yBottom, translatedThreshold); + + if (!isNull || connectNulls) { + + if (!connectNulls) { + addDummyPoints(i, i - 1, 'left'); + } + + if (!(isNull && !stacking && connectNulls)) { // Skip null point when stacking is false and connectNulls true + graphPoints.push(points[i]); + bottomPoints.push({ + x: i, + plotX: plotX, + plotY: yBottom + }); + } + + if (!connectNulls) { + addDummyPoints(i, i + 1, 'right'); + } + } + } + + topPath = getGraphPath.call(this, graphPoints, true, true); + + bottomPoints.reversed = true; + bottomPath = getGraphPath.call(this, bottomPoints, true, true); + if (bottomPath.length) { + bottomPath[0] = L; + } + + areaPath = topPath.concat(bottomPath); + graphPath = getGraphPath.call(this, graphPoints, false, connectNulls); // TODO: don't set leftCliff and rightCliff when connectNulls? + + this.areaPath = areaPath; + return graphPath; + }, + + /** + * Draw the graph and the underlying area. This method calls the Series base + * function and adds the area. The areaPath is calculated in the getSegmentPath + * method called from Series.prototype.drawGraph. + */ + drawGraph: function () { + + // Define or reset areaPath + this.areaPath = []; + + // Call the base method + Series.prototype.drawGraph.apply(this); + + // Define local variables + var series = this, + areaPath = this.areaPath, + options = this.options, + zones = this.zones, + props = [['area', this.color, options.fillColor]]; // area name, main color, fill color + + each(zones, function (threshold, i) { + props.push(['zoneArea' + i, threshold.color || series.color, threshold.fillColor || options.fillColor]); + }); + each(props, function (prop) { + var areaKey = prop[0], + area = series[areaKey], + attr; + + // Create or update the area + if (area) { // update + area.animate({ d: areaPath }); + + } else { // create + attr = { + fill: prop[2] || prop[1], + zIndex: 0 // #1069 + }; + if (!prop[2]) { + attr['fill-opacity'] = pick(options.fillOpacity, 0.75); + } + series[areaKey] = series.chart.renderer.path(areaPath) + .attr(attr) + .add(series.group); + } + }); + }, + + drawLegendSymbol: LegendSymbolMixin.drawRectangle + }); + + seriesTypes.area = AreaSeries; + /** + * Set the default options for spline + */ + defaultPlotOptions.spline = merge(defaultSeriesOptions); + + /** + * SplineSeries object + */ + var SplineSeries = extendClass(Series, { + type: 'spline', + + /** + * Get the spline segment from a given point's previous neighbour to the given point + */ + getPointSpline: function (points, 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 = points[i - 1], + nextPoint = points[i + 1], + leftContX, + leftContY, + rightContX, + rightContY, + ret; + + // Find control points + if (lastPoint && !lastPoint.isNull && nextPoint && !nextPoint.isNull) { + var lastX = lastPoint.plotX, + lastY = lastPoint.plotY, + nextX = nextPoint.plotX, + nextY = nextPoint.plotY, + correction = 0; + + 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 + if (rightContX !== leftContX) { // #5016, division by zero + 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; + + + } + + // Visualize control points for debugging + /* + if (leftContX) { + this.chart.renderer.circle(leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, 2) + .attr({ + stroke: 'red', + 'stroke-width': 1, + fill: 'none' + }) + .add(); + this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, + 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) + .attr({ + stroke: 'red', + 'stroke-width': 1 + }) + .add(); + this.chart.renderer.circle(rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, 2) + .attr({ + stroke: 'green', + 'stroke-width': 1, + fill: 'none' + }) + .add(); + this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, + 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) + .attr({ + stroke: 'green', + 'stroke-width': 1 + }) + .add(); + } + // */ + ret = [ + 'C', + pick(lastPoint.rightContX, lastPoint.plotX), + pick(lastPoint.rightContY, lastPoint.plotY), + pick(leftContX, plotX), + pick(leftContY, plotY), + plotX, + plotY + ]; + lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later + return ret; + } + }); + seriesTypes.spline = SplineSeries; + + /** + * Set the default options for areaspline + */ + defaultPlotOptions.areaspline = merge(defaultPlotOptions.area); + + /** + * AreaSplineSeries object + */ + var areaProto = AreaSeries.prototype, + AreaSplineSeries = extendClass(SplineSeries, { + type: 'areaspline', + getStackPoints: areaProto.getStackPoints, + getGraphPath: areaProto.getGraphPath, + setStackCliffs: areaProto.setStackCliffs, + drawGraph: areaProto.drawGraph, + drawLegendSymbol: LegendSymbolMixin.drawRectangle + }); + + seriesTypes.areaspline = AreaSplineSeries; + + /** + * Set the default options for column + */ + defaultPlotOptions.column = merge(defaultSeriesOptions, { + borderColor: '#FFFFFF', + //borderWidth: 1, + borderRadius: 0, + //colorByPoint: undefined, + groupPadding: 0.2, + //grouping: true, + marker: null, // point options are specified in the base options + pointPadding: 0.1, + //pointWidth: null, + minPointLength: 0, + cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes + pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories + states: { + hover: { + brightness: 0.1, + shadow: false, + halo: false + }, + select: { + color: '#C0C0C0', + borderColor: '#000000', + shadow: false + } + }, + dataLabels: { + align: null, // auto + verticalAlign: null, // auto + y: null + }, + softThreshold: false, + startFromThreshold: true, // false doesn't work well: http://jsfiddle.net/highcharts/hz8fopan/14/ + stickyTracking: false, + tooltip: { + distance: 6 + }, + threshold: 0 + }); + + /** + * ColumnSeries object + */ + var ColumnSeries = extendClass(Series, { + type: 'column', + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'borderColor', + fill: 'color', + r: 'borderRadius' + }, + cropShoulder: 0, + directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply. + trackerGroups: ['group', 'dataLabelsGroup'], + negStacks: true, // use separate negative stacks, unlike area stacks where a negative + // point is substracted from previous (#1910) + + /** + * Initialize the series + */ + init: function () { + Series.prototype.init.apply(this, arguments); + + var series = this, + chart = series.chart; + + // 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; + } + }); + } + }, + + /** + * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding, + * pointWidth etc. + */ + getColumnMetrics: function () { + + var series = this, + options = series.options, + xAxis = series.xAxis, + yAxis = series.yAxis, + reversedXAxis = xAxis.reversed, + stackKey, + stackGroups = {}, + columnCount = 0; + + // 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 + if (options.grouping === false) { + columnCount = 1; + } else { + each(series.chart.series, function (otherSeries) { + var otherOptions = otherSeries.options, + otherYAxis = otherSeries.yAxis, + columnIndex; + if (otherSeries.type === series.type && otherSeries.visible && + yAxis.len === otherYAxis.len && yAxis.pos === otherYAxis.pos) { // #642, #2086 + if (otherOptions.stacking) { + stackKey = otherSeries.stackKey; + if (stackGroups[stackKey] === UNDEFINED) { + stackGroups[stackKey] = columnCount++; + } + columnIndex = stackGroups[stackKey]; + } else if (otherOptions.grouping !== false) { // #1162 + columnIndex = columnCount++; + } + otherSeries.columnIndex = columnIndex; + } + }); + } + + var categoryWidth = mathMin( + mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || xAxis.tickInterval || 1), // #2610 + xAxis.len // #1535 + ), + groupPadding = categoryWidth * options.groupPadding, + groupWidth = categoryWidth - 2 * groupPadding, + pointOffsetWidth = groupWidth / columnCount, + pointWidth = mathMin( + options.maxPointWidth || xAxis.len, + pick(options.pointWidth, pointOffsetWidth * (1 - 2 * options.pointPadding)) + ), + pointPadding = (pointOffsetWidth - pointWidth) / 2, + colIndex = (series.columnIndex || 0) + (reversedXAxis ? 1 : 0), // #1251, #3737 + pointXOffset = pointPadding + (groupPadding + colIndex * + pointOffsetWidth - (categoryWidth / 2)) * + (reversedXAxis ? -1 : 1); + + // Save it for reading in linked series (Error bars particularly) + series.columnMetrics = { + width: pointWidth, + offset: pointXOffset + }; + return series.columnMetrics; + + }, + + /** + * Make the columns crisp. The edges are rounded to the nearest full pixel. + */ + crispCol: function (x, y, w, h) { + var chart = this.chart, + borderWidth = this.borderWidth, + xCrisp = -(borderWidth % 2 ? 0.5 : 0), + yCrisp = borderWidth % 2 ? 0.5 : 1, + right, + bottom, + fromTop; + + if (chart.inverted && chart.renderer.isVML) { + yCrisp += 1; + } + + // Horizontal. We need to first compute the exact right edge, then round it + // and compute the width from there. + right = Math.round(x + w) + xCrisp; + x = Math.round(x) + xCrisp; + w = right - x; + + // Vertical + bottom = Math.round(y + h) + yCrisp; + fromTop = mathAbs(y) <= 0.5 && bottom > 0.5; // #4504, #4656 + y = Math.round(y) + yCrisp; + h = bottom - y; + + // Top edges are exceptions + if (fromTop && h) { // #5146 + y -= 1; + h += 1; + } + + return { + x: x, + y: y, + width: w, + height: h + }; + }, + + /** + * Translate each point to the plot area coordinate system and find shape positions + */ + translate: function () { + var series = this, + chart = series.chart, + options = series.options, + borderWidth = series.borderWidth = pick( + options.borderWidth, + series.closestPointRange * series.xAxis.transA < 2 ? 0 : 1 // #3635 + ), + yAxis = series.yAxis, + threshold = options.threshold, + translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold), + minPointLength = pick(options.minPointLength, 5), + metrics = series.getColumnMetrics(), + pointWidth = metrics.width, + seriesBarW = series.barW = mathMax(pointWidth, 1 + 2 * borderWidth), // postprocessed for border width + pointXOffset = series.pointXOffset = metrics.offset; + + if (chart.inverted) { + translatedThreshold -= 0.5; // #3355 + } + + // When the pointPadding is 0, we want the columns to be packed tightly, so we allow individual + // columns to have individual sizes. When pointPadding is greater, we strive for equal-width + // columns (#2694). + if (options.pointPadding) { + seriesBarW = mathCeil(seriesBarW); + } + + Series.prototype.translate.apply(series); + + // Record the new values + each(series.points, function (point) { + var yBottom = mathMin(pick(point.yBottom, translatedThreshold), 9e4), // #3575 + safeDistance = 999 + mathAbs(yBottom), + plotY = mathMin(mathMax(-safeDistance, point.plotY), yAxis.len + safeDistance), // Don't draw too far outside plot area (#1303, #2241, #4264) + barX = point.plotX + pointXOffset, + barW = seriesBarW, + barY = mathMin(plotY, yBottom), + up, + barH = mathMax(plotY, yBottom) - barY; + + // Handle options.minPointLength + if (mathAbs(barH) < minPointLength) { + if (minPointLength) { + barH = minPointLength; + up = (!yAxis.reversed && !point.negative) || (yAxis.reversed && point.negative); + barY = mathAbs(barY - translatedThreshold) > minPointLength ? // stacked + yBottom - minPointLength : // keep position + translatedThreshold - (up ? minPointLength : 0); // #1485, #4051 + } + } + + // Cache for access in polar + point.barX = barX; + point.pointWidth = pointWidth; + + // Fix the tooltip on center of grouped columns (#1216, #424, #3648) + point.tooltipPos = chart.inverted ? + [yAxis.len + yAxis.pos - chart.plotLeft - plotY, series.xAxis.len - barX - barW / 2, barH] : + [barX + barW / 2, plotY + yAxis.pos - chart.plotTop, barH]; + + // Register shape type and arguments to be used in drawPoints + point.shapeType = 'rect'; + point.shapeArgs = series.crispCol(barX, barY, barW, barH); + }); + + }, + + getSymbol: noop, + + /** + * Use a solid rectangle like the area series types + */ + drawLegendSymbol: LegendSymbolMixin.drawRectangle, + + + /** + * Columns have no graph + */ + drawGraph: noop, + + /** + * 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, + chart = this.chart, + options = series.options, + renderer = chart.renderer, + animationLimit = options.animationLimit || 250, + shapeArgs, + pointAttr; + + // draw the columns + each(series.points, function (point) { + var plotY = point.plotY, + graphic = point.graphic, + borderAttr; + + if (isNumber(plotY) && point.y !== null) { + shapeArgs = point.shapeArgs; + + borderAttr = defined(series.borderWidth) ? { + 'stroke-width': series.borderWidth + } : {}; + + pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || series.pointAttr[NORMAL_STATE]; + + if (graphic) { // update + stop(graphic); + graphic.attr(borderAttr).attr(pointAttr)[chart.pointCount < animationLimit ? 'animate' : 'attr'](merge(shapeArgs)); // #4267 + + } else { + point.graphic = graphic = renderer[point.shapeType](shapeArgs) + .attr(borderAttr) + .attr(pointAttr) + .add(point.group || series.group) + .shadow(options.shadow, null, options.stacking && !options.borderRadius); + } + + } else if (graphic) { + point.graphic = graphic.destroy(); // #1269 + } + }); + }, + + /** + * 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, + yAxis = this.yAxis, + options = series.options, + inverted = this.chart.inverted, + attr = {}, + translatedThreshold; + + if (hasSVG) { // VML is too slow anyway + if (init) { + attr.scaleY = 0.001; + translatedThreshold = mathMin(yAxis.pos + yAxis.len, mathMax(yAxis.pos, yAxis.toPixels(options.threshold))); + if (inverted) { + attr.translateX = translatedThreshold - yAxis.len; + } else { + attr.translateY = translatedThreshold; + } + series.group.attr(attr); + + } else { // run the animation + + attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos; + series.group.animate(attr, extend(animObject(series.options.animation), { + // Do the scale synchronously to ensure smooth updating (#5030) + step: function (val, fx) { + series.group.attr({ + scaleY: mathMax(0.001, fx.pos) // #5250 + }); + } + })); + + // 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; + /** + * Set the default options for bar + */ + defaultPlotOptions.bar = merge(defaultPlotOptions.column); + /** + * The Bar series class + */ + var BarSeries = extendClass(ColumnSeries, { + type: 'bar', + inverted: true + }); + seriesTypes.bar = BarSeries; + + /** + * Set the default options for scatter + */ + defaultPlotOptions.scatter = merge(defaultSeriesOptions, { + lineWidth: 0, + marker: { + enabled: true // Overrides auto-enabling in line series (#3647) + }, + tooltip: { + headerFormat: '\u25CF {series.name}
', + pointFormat: 'x: {point.x}
y: {point.y}
' + } + }); + + /** + * The scatter series class + */ + var ScatterSeries = extendClass(Series, { + type: 'scatter', + sorted: false, + requireSorting: false, + noSharedTooltip: true, + trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'], + takeOrdinalPosition: false, // #2342 + kdDimensions: 2, + drawGraph: function () { + if (this.options.lineWidth) { + Series.prototype.drawGraph.call(this); + } + } + }); + + seriesTypes.scatter = ScatterSeries; + + /** + * Set the default options for pie + */ + defaultPlotOptions.pie = merge(defaultSeriesOptions, { + borderColor: '#FFFFFF', + borderWidth: 1, + center: [null, null], + clip: false, + colorByPoint: true, // always true for pies + dataLabels: { + // align: null, + // connectorWidth: 1, + // connectorColor: point.color, + // connectorPadding: 5, + distance: 30, + enabled: true, + formatter: function () { // #2945 + return this.y === null ? undefined : this.point.name; + }, + // softConnector: true, + x: 0 + // y: 0 + }, + ignoreHiddenPoint: true, + //innerSize: 0, + legendType: 'point', + marker: null, // point options are specified in the base options + size: null, + showInLegend: false, + slicedOffset: 10, + states: { + hover: { + brightness: 0.1, + shadow: false + } + }, + stickyTracking: false, + tooltip: { + followPointer: true + } + }); + + /** + * 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; + + point.name = pick(point.name, 'Slice'); + + // add event listener for select + toggleSlice = function (e) { + point.slice(e.type === 'select'); + }; + 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, redraw) { + var point = this, + series = point.series, + chart = series.chart, + ignoreHiddenPoint = series.options.ignoreHiddenPoint; + + redraw = pick(redraw, ignoreHiddenPoint); + + if (vis !== point.visible) { + + // If called without an argument, toggle visibility + point.visible = point.options.visible = vis = vis === UNDEFINED ? !point.visible : vis; + series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data + + // Show and hide associated elements. This is performed regardless of redraw or not, + // because chart.redraw only handles full series. + each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) { + if (point[key]) { + point[key][vis ? 'show' : 'hide'](true); + } + }); + + if (point.legendItem) { + chart.legend.colorizeItem(point, vis); + } + + // #4170, hide halo after hiding point + if (!vis && point.state === 'hover') { + point.setState(''); + } + + // Handle ignore hidden slices + if (ignoreHiddenPoint) { + series.isDirty = true; + } + + if (redraw) { + chart.redraw(); + } + } + }, + + /** + * 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, + translation; + + setAnimation(animation, chart); + + // redraw is true by default + redraw = pick(redraw, true); + + // if called without an argument, toggle + point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced; + series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data + + translation = sliced ? point.slicedTranslation : { + translateX: 0, + translateY: 0 + }; + + point.graphic.animate(translation); + + if (point.shadowGroup) { + point.shadowGroup.animate(translation); + } + + }, + + haloPath: function (size) { + var shapeArgs = this.shapeArgs, + chart = this.series.chart; + + return this.sliced || !this.visible ? [] : this.series.chart.renderer.symbols.arc(chart.plotLeft + shapeArgs.x, chart.plotTop + shapeArgs.y, shapeArgs.r + size, shapeArgs.r + size, { + innerR: this.shapeArgs.r, + start: shapeArgs.start, + end: shapeArgs.end + }); + } + }); + + /** + * The Pie series class + */ + var PieSeries = { + type: 'pie', + isCartesian: false, + pointClass: PiePoint, + requireSorting: false, + directTouch: true, + noSharedTooltip: true, + trackerGroups: ['group', 'dataLabelsGroup'], + axisTypes: [], + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'borderColor', + 'stroke-width': 'borderWidth', + fill: 'color' + }, + + /** + * Animate the pies in + */ + animate: function (init) { + var series = this, + points = series.points, + startAngleRad = series.startAngleRad; + + if (!init) { + each(points, function (point) { + var graphic = point.graphic, + args = point.shapeArgs; + + if (graphic) { + // start values + graphic.attr({ + r: point.startR || (series.center[3] / 2), // animate from inner radius (#779) + start: startAngleRad, + end: startAngleRad + }); + + // 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; + } + }, + + /** + * Recompute total chart sum and update percentages of points. + */ + updateTotals: function () { + var i, + total = 0, + points = this.points, + len = points.length, + point, + ignoreHiddenPoint = this.options.ignoreHiddenPoint; + + // Get the total sum + for (i = 0; i < len; i++) { + point = points[i]; + total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y; + } + this.total = total; + + // Set each point's properties + for (i = 0; i < len; i++) { + point = points[i]; + point.percentage = (total > 0 && (point.visible || !ignoreHiddenPoint)) ? point.y / total * 100 : 0; + point.total = total; + } + }, + + /** + * Extend the generatePoints method by adding total and percentage properties to each point + */ + generatePoints: function () { + Series.prototype.generatePoints.call(this); + this.updateTotals(); + }, + + /** + * Do translation for pie slices + */ + translate: function (positions) { + this.generatePoints(); + + var series = this, + cumulative = 0, + precision = 1000, // issue #172 + options = series.options, + slicedOffset = options.slicedOffset, + connectorOffset = slicedOffset + options.borderWidth, + start, + end, + angle, + startAngle = options.startAngle || 0, + startAngleRad = series.startAngleRad = mathPI / 180 * (startAngle - 90), + endAngleRad = series.endAngleRad = mathPI / 180 * ((pick(options.endAngle, startAngle + 360)) - 90), + circ = endAngleRad - startAngleRad, //2 * mathPI, + points = series.points, + radiusX, // the x component of the radius vector for a given point + radiusY, + labelDistance = options.dataLabels.distance, + ignoreHiddenPoint = options.ignoreHiddenPoint, + i, + len = points.length, + point; + + // Get positions - either an integer or a percentage string must be given. + // If positions are passed as a parameter, we're in a recursive loop for adjusting + // space for data labels. + if (!positions) { + series.center = positions = series.getCenter(); + } + + // 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(mathMin((y - positions[1]) / (positions[2] / 2 + labelDistance), 1)); + + return positions[0] + + (left ? -1 : 1) * + (mathCos(angle) * (positions[2] / 2 + labelDistance)); + }; + + // Calculate the geometry for each point + for (i = 0; i < len; i++) { + + point = points[i]; + + // set start and end angle + start = startAngleRad + (cumulative * circ); + if (!ignoreHiddenPoint || point.visible) { + cumulative += point.percentage / 100; + } + end = startAngleRad + (cumulative * circ); + + // set the shape + point.shapeType = 'arc'; + point.shapeArgs = { + x: positions[0], + y: positions[1], + r: positions[2] / 2, + innerR: positions[3] / 2, + start: mathRound(start * precision) / precision, + end: mathRound(end * precision) / precision + }; + + // The angle must stay within -90 and 270 (#2645) + angle = (end + start) / 2; + if (angle > 1.5 * mathPI) { + angle -= 2 * mathPI; + } else if (angle < -mathPI / 2) { + angle += 2 * mathPI; + } + + // Center for the sliced out slice + point.slicedTranslation = { + translateX: mathRound(mathCos(angle) * slicedOffset), + translateY: mathRound(mathSin(angle) * slicedOffset) + }; + + // 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 + ]; + + point.half = angle < -mathPI / 2 || angle > mathPI / 2 ? 1 : 0; + point.angle = angle; + + // set the anchor point for data labels + connectorOffset = mathMin(connectorOffset, labelDistance / 2); // #1678 + 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' : + point.half ? 'right' : 'left', // alignment + angle // center angle + ]; + + } + }, + + drawGraph: null, + + /** + * Draw the data points + */ + drawPoints: function () { + var series = this, + chart = series.chart, + renderer = chart.renderer, + groupTranslation, + //center, + graphic, + //group, + shadow = series.options.shadow, + shadowGroup, + pointAttr, + shapeArgs, + attr; + + if (shadow && !series.shadowGroup) { + series.shadowGroup = renderer.g('shadow') + .add(series.group); + } + + // draw the slices + each(series.points, function (point) { + if (point.y !== null) { + graphic = point.graphic; + shapeArgs = point.shapeArgs; + shadowGroup = point.shadowGroup; + pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]; + if (!pointAttr.stroke) { + pointAttr.stroke = pointAttr.fill; + } + + // put the shadow behind all points + if (shadow && !shadowGroup) { + shadowGroup = point.shadowGroup = renderer.g('shadow') + .add(series.shadowGroup); + } + + // if the point is sliced, use special translation, else use plot area traslation + groupTranslation = point.sliced ? point.slicedTranslation : { + translateX: 0, + translateY: 0 + }; + + //group.translate(groupTranslation[0], groupTranslation[1]); + if (shadowGroup) { + shadowGroup.attr(groupTranslation); + } + + // draw the slice + if (graphic) { + graphic + .setRadialReference(series.center) + .attr(pointAttr) + .animate(extend(shapeArgs, groupTranslation)); + } else { + attr = { 'stroke-linejoin': 'round' }; + if (!point.visible) { + attr.visibility = 'hidden'; + } + + point.graphic = graphic = renderer[point.shapeType](shapeArgs) + .setRadialReference(series.center) + .attr(pointAttr) + .attr(attr) + .attr(groupTranslation) + .add(series.group) + .shadow(shadow, shadowGroup); + } + } + }); + + }, + + + searchPoint: noop, + + /** + * Utility for sorting data labels + */ + sortByAngle: function (points, sign) { + points.sort(function (a, b) { + return a.angle !== undefined && (b.angle - a.angle) * sign; + }); + }, + + /** + * Use a simple symbol from LegendSymbolMixin + */ + drawLegendSymbol: LegendSymbolMixin.drawRectangle, + + /** + * Use the getCenter method from drawLegendSymbol + */ + getCenter: CenteredSeriesMixin.getCenter, + + /** + * Pies don't have point marker symbols + */ + getSymbol: noop + + }; + PieSeries = extendClass(Series, PieSeries); + seriesTypes.pie = PieSeries; + + /** + * Draw the data labels + */ + Series.prototype.drawDataLabels = function () { + + var series = this, + seriesOptions = series.options, + cursor = seriesOptions.cursor, + options = seriesOptions.dataLabels, + points = series.points, + pointOptions, + generalOptions, + hasRendered = series.hasRendered || 0, + str, + dataLabelsGroup, + defer = pick(options.defer, true), + renderer = series.chart.renderer; + + if (options.enabled || series._hasPointLabels) { + + // Process default alignment of data labels for columns + if (series.dlProcessOptions) { + series.dlProcessOptions(options); + } + + // Create a separate group for the data labels to avoid rotation + dataLabelsGroup = series.plotGroup( + 'dataLabelsGroup', + 'data-labels', + defer && !hasRendered ? 'hidden' : 'visible', // #5133 + options.zIndex || 6 + ); + + if (defer) { + dataLabelsGroup.attr({ opacity: +hasRendered }); // #3300 + if (!hasRendered) { + addEvent(series, 'afterAnimate', function () { + if (series.visible) { // #3023, #3024 + dataLabelsGroup.show(); + } + dataLabelsGroup[seriesOptions.animation ? 'animate' : 'attr']({ opacity: 1 }, { duration: 200 }); + }); + } + } + + // Make the labels for each point + generalOptions = options; + each(points, function (point) { + + var enabled, + dataLabel = point.dataLabel, + labelConfig, + attr, + name, + rotation, + connector = point.connector, + isNew = true, + style, + moreStyle = {}; + + // Determine if each data label is enabled + pointOptions = point.dlOptions || (point.options && point.options.dataLabels); // dlOptions is used in treemaps + enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled) && point.y !== null; // #2282, #4641 + + + // If the point is outside the plot area, destroy it. #678, #820 + if (dataLabel && !enabled) { + point.dataLabel = dataLabel.destroy(); + + // Individual labels are disabled if the are explicitly disabled + // in the point options, or if they fall outside the plot area. + } else if (enabled) { + + // Create individual options structure that can be extended without + // affecting others + options = merge(generalOptions, pointOptions); + style = options.style; + + rotation = options.rotation; + + // Get the string + labelConfig = point.getLabelConfig(); + str = options.format ? + format(options.format, labelConfig) : + options.formatter.call(labelConfig, options); + + // Determine the color + style.color = pick(options.color, style.color, series.color, 'black'); + + + // update existing label + if (dataLabel) { + + if (defined(str)) { + dataLabel + .attr({ + text: str + }); + isNew = false; + + } else { // #1437 - the label is shown conditionally + point.dataLabel = dataLabel = dataLabel.destroy(); + if (connector) { + point.connector = connector.destroy(); + } + } + + // create new label + } else if (defined(str)) { + attr = { + //align: align, + fill: options.backgroundColor, + stroke: options.borderColor, + 'stroke-width': options.borderWidth, + r: options.borderRadius || 0, + rotation: rotation, + padding: options.padding, + zIndex: 1 + }; + + // Get automated contrast color + if (style.color === 'contrast') { + moreStyle.color = options.inside || options.distance < 0 || !!seriesOptions.stacking ? + renderer.getContrast(point.color || series.color) : + '#000000'; + } + if (cursor) { + moreStyle.cursor = cursor; + } + + + // Remove unused attributes (#947) + for (name in attr) { + if (attr[name] === UNDEFINED) { + delete attr[name]; + } + } + + dataLabel = point.dataLabel = renderer[rotation ? 'text' : 'label']( // labels don't support rotation + str, + 0, + -9999, + options.shape, + null, + null, + options.useHTML + ) + .attr(attr) + .css(extend(style, moreStyle)) + .add(dataLabelsGroup) + .shadow(options.shadow); + + } + + if (dataLabel) { + // Now the data label is created and placed at 0,0, so we need to align it + series.alignDataLabel(point, dataLabel, options, null, isNew); + } + } + }); + } + }; + + /** + * Align each individual data label + */ + Series.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) { + var chart = this.chart, + inverted = chart.inverted, + plotX = pick(point.plotX, -9999), + plotY = pick(point.plotY, -9999), + bBox = dataLabel.getBBox(), + baseline = chart.renderer.fontMetrics(options.style.fontSize).b, + rotation = options.rotation, + normRotation, + negRotation, + align = options.align, + rotCorr, // rotation correction + // Math.round for rounding errors (#2683), alignTo to allow column labels (#2700) + visible = this.visible && (point.series.forceDL || chart.isInsidePlot(plotX, mathRound(plotY), inverted) || + (alignTo && chart.isInsidePlot(plotX, inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1, inverted))), + alignAttr, // the final position; + justify = pick(options.overflow, 'justify') === 'justify'; + + if (visible) { + + // The alignment box is a singular point + alignTo = extend({ + x: inverted ? chart.plotWidth - plotY : plotX, + y: mathRound(inverted ? chart.plotHeight - plotX : plotY), + width: 0, + height: 0 + }, alignTo); + + // Add the text size for alignment calculation + extend(options, { + width: bBox.width, + height: bBox.height + }); + + // Allow a hook for changing alignment in the last moment, then do the alignment + if (rotation) { + justify = false; // Not supported for rotated text + rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723 + alignAttr = { + x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x, + y: alignTo.y + options.y + { top: 0, middle: 0.5, bottom: 1 }[options.verticalAlign] * alignTo.height + }; + dataLabel[isNew ? 'attr' : 'animate'](alignAttr) + .attr({ // #3003 + align: align + }); + + // Compensate for the rotated label sticking out on the sides + normRotation = (rotation + 720) % 360; + negRotation = normRotation > 180 && normRotation < 360; + + if (align === 'left') { + alignAttr.y -= negRotation ? bBox.height : 0; + } else if (align === 'center') { + alignAttr.x -= bBox.width / 2; + alignAttr.y -= bBox.height / 2; + } else if (align === 'right') { + alignAttr.x -= bBox.width; + alignAttr.y -= negRotation ? 0 : bBox.height; + } + + + } else { + dataLabel.align(options, null, alignTo); + alignAttr = dataLabel.alignAttr; + } + + // Handle justify or crop + if (justify) { + this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew); + + // Now check that the data label is within the plot area + } else if (pick(options.crop, true)) { + visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height); + } + + // When we're using a shape, make it possible with a connector or an arrow pointing to thie point + if (options.shape && !rotation) { + dataLabel.attr({ + anchorX: point.plotX, + anchorY: point.plotY + }); + } + } + + // Show or hide based on the final aligned position + if (!visible) { + stop(dataLabel); + dataLabel.attr({ y: -9999 }); + dataLabel.placed = false; // don't animate back in + } + + }; + + /** + * If data labels fall partly outside the plot area, align them back in, in a way that + * doesn't hide the point. + */ + Series.prototype.justifyDataLabel = function (dataLabel, options, alignAttr, bBox, alignTo, isNew) { + var chart = this.chart, + align = options.align, + verticalAlign = options.verticalAlign, + off, + justified, + padding = dataLabel.box ? 0 : (dataLabel.padding || 0); + + // Off left + off = alignAttr.x + padding; + if (off < 0) { + if (align === 'right') { + options.align = 'left'; + } else { + options.x = -off; + } + justified = true; + } + + // Off right + off = alignAttr.x + bBox.width - padding; + if (off > chart.plotWidth) { + if (align === 'left') { + options.align = 'right'; + } else { + options.x = chart.plotWidth - off; + } + justified = true; + } + + // Off top + off = alignAttr.y + padding; + if (off < 0) { + if (verticalAlign === 'bottom') { + options.verticalAlign = 'top'; + } else { + options.y = -off; + } + justified = true; + } + + // Off bottom + off = alignAttr.y + bBox.height - padding; + if (off > chart.plotHeight) { + if (verticalAlign === 'top') { + options.verticalAlign = 'bottom'; + } else { + options.y = chart.plotHeight - off; + } + justified = true; + } + + if (justified) { + dataLabel.placed = !isNew; + dataLabel.align(options, null, alignTo); + } + }; + + /** + * Override the base drawDataLabels method by pie specific functionality + */ + if (seriesTypes.pie) { + seriesTypes.pie.prototype.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), + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + connector, + connectorPath, + softConnector = pick(options.softConnector, true), + distanceOption = options.distance, + seriesCenter = series.center, + radius = seriesCenter[2] / 2, + centerY = seriesCenter[1], + outside = distanceOption > 0, + dataLabel, + dataLabelWidth, + labelPos, + labelHeight, + halves = [// divide the points into right and left halves for anti collision + [], // right + [] // left + ], + x, + y, + visibility, + rankArr, + i, + j, + overflow = [0, 0, 0, 0], // top, right, bottom, left + sort = function (a, b) { + return b.y - a.y; + }; + + // get out if not enabled + if (!series.visible || (!options.enabled && !series._hasPointLabels)) { + return; + } + + // run parent method + Series.prototype.drawDataLabels.apply(series); + + each(data, function (point) { + if (point.dataLabel && point.visible) { // #407, #2510 + + // Arrange points for detection collision + halves[point.half].push(point); + + // Reset positions (#4905) + point.dataLabel._pos = null; + } + }); + + /* Loop over the points in each half, starting from the top and bottom + * of the pie to detect overlapping labels. + */ + i = 2; + while (i--) { + + var slots = [], + slotsLength, + usedSlots = [], + points = halves[i], + pos, + bottom, + length = points.length, + slotIndex; + + if (!length) { + continue; + } + + // Sort by angle + series.sortByAngle(points, i - 0.5); + + // Assume equal label heights on either hemisphere (#2630) + j = labelHeight = 0; + while (!labelHeight && points[j]) { // #1569 + labelHeight = points[j] && points[j].dataLabel && (points[j].dataLabel.getBBox().height || 21); // 21 is for #968 + j++; + } + + // Only do anti-collision when we are outside the pie and have connectors (#856) + if (distanceOption > 0) { + + // Build the slots + bottom = mathMin(centerY + radius + distanceOption, chart.plotHeight); + for (pos = mathMax(0, centerY - radius - distanceOption); pos <= bottom; pos += labelHeight) { + slots.push(pos); + } + slotsLength = slots.length; + + + /* Visualize the slots + if (!series.slotElements) { + series.slotElements = []; + } + if (i === 1) { + series.slotElements.forEach(function (elem) { + elem.destroy(); + }); + series.slotElements.length = 0; + } + + slots.forEach(function (pos, no) { + var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0), + slotY = pos + chart.plotTop; + + if (isNumber(slotX)) { + series.slotElements.push(chart.renderer.rect(slotX, slotY - 7, 100, labelHeight, 1) + .attr({ + 'stroke-width': 1, + stroke: 'silver', + fill: 'rgba(0,0,255,0.1)' + }) + .add()); + series.slotElements.push(chart.renderer.text('Slot '+ no, slotX, slotY + 4) + .attr({ + fill: 'silver' + }).add()); + } + }); + // */ + + // 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++) { + + var slot, naturalY; + + point = points[j]; + labelPos = point.labelPos; + dataLabel = point.dataLabel; + visibility = point.visible === false ? HIDDEN : 'inherit'; + naturalY = labelPos[1]; + + if (distanceOption > 0) { + slot = usedSlots.pop(); + 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 = mathMin(mathMax(0, naturalY), chart.plotHeight); + } + + } else { + 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 = options.justify ? + seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption) : + series.getX(y === centerY - radius - distanceOption || y === centerY + radius + distanceOption ? naturalY : y, i); + + + // Record the placement and visibility + dataLabel._attr = { + visibility: visibility, + align: labelPos[6] + }; + dataLabel._pos = { + x: x + options.x + + ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0), + y: y + options.y - 10 // 10 is for the baseline (label vs text) + }; + dataLabel.connX = x; + dataLabel.connY = y; + + + // Detect overflowing data labels + if (this.options.size === null) { + dataLabelWidth = dataLabel.width; + // Overflow left + if (x - dataLabelWidth < connectorPadding) { + overflow[3] = mathMax(mathRound(dataLabelWidth - x + connectorPadding), overflow[3]); + + // Overflow right + } else if (x + dataLabelWidth > plotWidth - connectorPadding) { + overflow[1] = mathMax(mathRound(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]); + } + + // Overflow top + if (y - labelHeight / 2 < 0) { + overflow[0] = mathMax(mathRound(-y + labelHeight / 2), overflow[0]); + + // Overflow left + } else if (y + labelHeight / 2 > plotHeight) { + overflow[2] = mathMax(mathRound(y + labelHeight / 2 - plotHeight), overflow[2]); + } + } + } // for each point + } // for each half + + // Do not apply the final placement and draw the connectors until we have verified + // that labels are not spilling over. + if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) { + + // Place the labels in the final position + this.placeDataLabels(); + + // Draw the connectors + if (outside && connectorWidth) { + each(this.points, function (point) { + connector = point.connector; + labelPos = point.labelPos; + dataLabel = point.dataLabel; + + if (dataLabel && dataLabel._pos && point.visible) { + visibility = dataLabel._attr.visibility; + x = dataLabel.connX; + y = dataLabel.connY; + 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: 0 // #2722 (reversed) + }) + .add(series.dataLabelsGroup); + } + } else if (connector) { + point.connector = connector.destroy(); + } + }); + } + } + }; + /** + * Perform the final placement of the data labels after we have verified that they + * fall within the plot area. + */ + seriesTypes.pie.prototype.placeDataLabels = function () { + each(this.points, function (point) { + var dataLabel = point.dataLabel, + _pos; + + if (dataLabel && point.visible) { + _pos = dataLabel._pos; + if (_pos) { + dataLabel.attr(dataLabel._attr); + dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos); + dataLabel.moved = true; + } else if (dataLabel) { + dataLabel.attr({ y: -9999 }); + } + } + }); + }; + + seriesTypes.pie.prototype.alignDataLabel = noop; + + /** + * Verify whether the data labels are allowed to draw, or we should run more translation and data + * label positioning to keep them inside the plot area. Returns true when data labels are ready + * to draw. + */ + seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) { + + var center = this.center, + options = this.options, + centerOption = options.center, + minSize = options.minSize || 80, + newSize = minSize, + ret; + + // Handle horizontal size and center + if (centerOption[0] !== null) { // Fixed center + newSize = mathMax(center[2] - mathMax(overflow[1], overflow[3]), minSize); + + } else { // Auto center + newSize = mathMax( + center[2] - overflow[1] - overflow[3], // horizontal overflow + minSize + ); + center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center + } + + // Handle vertical size and center + if (centerOption[1] !== null) { // Fixed center + newSize = mathMax(mathMin(newSize, center[2] - mathMax(overflow[0], overflow[2])), minSize); + + } else { // Auto center + newSize = mathMax( + mathMin( + newSize, + center[2] - overflow[0] - overflow[2] // vertical overflow + ), + minSize + ); + center[1] += (overflow[0] - overflow[2]) / 2; // vertical center + } + + // If the size must be decreased, we need to run translate and drawDataLabels again + if (newSize < center[2]) { + center[2] = newSize; + center[3] = Math.min(relativeLength(options.innerSize || 0, newSize), newSize); // #3632 + this.translate(center); + + if (this.drawDataLabels) { + this.drawDataLabels(); + } + // Else, return true to indicate that the pie and its labels is within the plot area + } else { + ret = true; + } + return ret; + }; + } + + if (seriesTypes.column) { + + /** + * Override the basic data label alignment by adjusting for the position of the column + */ + seriesTypes.column.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) { + var inverted = this.chart.inverted, + series = point.series, + dlBox = point.dlBox || point.shapeArgs, // data label box for alignment + below = pick(point.below, point.plotY > pick(this.translatedThreshold, series.yAxis.len)), // point.below is used in range series + inside = pick(options.inside, !!this.options.stacking), // draw it inside the box? + overshoot; + + // Align to the column itself, or the top of it + if (dlBox) { // Area range uses this method but not alignTo + alignTo = merge(dlBox); + + if (alignTo.y < 0) { + alignTo.height += alignTo.y; + alignTo.y = 0; + } + overshoot = alignTo.y + alignTo.height - series.yAxis.len; + if (overshoot > 0) { + alignTo.height -= overshoot; + } + + if (inverted) { + alignTo = { + x: series.yAxis.len - alignTo.y - alignTo.height, + y: series.xAxis.len - alignTo.x - alignTo.width, + width: alignTo.height, + height: alignTo.width + }; + } + + // Compute the alignment box + if (!inside) { + if (inverted) { + alignTo.x += below ? 0 : alignTo.width; + alignTo.width = 0; + } else { + alignTo.y += below ? alignTo.height : 0; + alignTo.height = 0; + } + } + } + + + // When alignment is undefined (typically columns and bars), display the individual + // point below or above the point depending on the threshold + options.align = pick( + options.align, + !inverted || inside ? 'center' : below ? 'right' : 'left' + ); + options.verticalAlign = pick( + options.verticalAlign, + inverted || inside ? 'middle' : below ? 'top' : 'bottom' + ); + + // Call the parent method + Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew); + }; + } + + + + /** + * Highcharts module to hide overlapping data labels. This module is included in Highcharts. + */ + (function (H) { + var Chart = H.Chart, + each = H.each, + pick = H.pick, + addEvent = H.addEvent; + + // Collect potensial overlapping data labels. Stack labels probably don't need to be + // considered because they are usually accompanied by data labels that lie inside the columns. + Chart.prototype.callbacks.push(function (chart) { + function collectAndHide() { + var labels = []; + + each(chart.series, function (series) { + var dlOptions = series.options.dataLabels, + collections = series.dataLabelCollections || ['dataLabel']; // Range series have two collections + if ((dlOptions.enabled || series._hasPointLabels) && !dlOptions.allowOverlap && series.visible) { // #3866 + each(collections, function (coll) { + each(series.points, function (point) { + if (point[coll]) { + point[coll].labelrank = pick(point.labelrank, point.shapeArgs && point.shapeArgs.height); // #4118 + labels.push(point[coll]); + } + }); + }); + } + }); + chart.hideOverlappingLabels(labels); + } + + // Do it now ... + collectAndHide(); + + // ... and after each chart redraw + addEvent(chart, 'redraw', collectAndHide); + + }); + + /** + * Hide overlapping labels. Labels are moved and faded in and out on zoom to provide a smooth + * visual imression. + */ + Chart.prototype.hideOverlappingLabels = function (labels) { + + var len = labels.length, + label, + i, + j, + label1, + label2, + isIntersecting, + pos1, + pos2, + parent1, + parent2, + padding, + intersectRect = function (x1, y1, w1, h1, x2, y2, w2, h2) { + return !( + x2 > x1 + w1 || + x2 + w2 < x1 || + y2 > y1 + h1 || + y2 + h2 < y1 + ); + }; + + // Mark with initial opacity + for (i = 0; i < len; i++) { + label = labels[i]; + if (label) { + label.oldOpacity = label.opacity; + label.newOpacity = 1; + } + } + + // Prevent a situation in a gradually rising slope, that each label + // will hide the previous one because the previous one always has + // lower rank. + labels.sort(function (a, b) { + return (b.labelrank || 0) - (a.labelrank || 0); + }); + + // Detect overlapping labels + for (i = 0; i < len; i++) { + label1 = labels[i]; + + for (j = i + 1; j < len; ++j) { + label2 = labels[j]; + if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0) { + pos1 = label1.alignAttr; + pos2 = label2.alignAttr; + parent1 = label1.parentGroup; // Different panes have different positions + parent2 = label2.parentGroup; + padding = 2 * (label1.box ? 0 : label1.padding); // Substract the padding if no background or border (#4333) + isIntersecting = intersectRect( + pos1.x + parent1.translateX, + pos1.y + parent1.translateY, + label1.width - padding, + label1.height - padding, + pos2.x + parent2.translateX, + pos2.y + parent2.translateY, + label2.width - padding, + label2.height - padding + ); + + if (isIntersecting) { + (label1.labelrank < label2.labelrank ? label1 : label2).newOpacity = 0; + } + } + } + } + + // Hide or show + each(labels, function (label) { + var complete, + newOpacity; + + if (label) { + newOpacity = label.newOpacity; + + if (label.oldOpacity !== newOpacity && label.placed) { + + // Make sure the label is completely hidden to avoid catching clicks (#4362) + if (newOpacity) { + label.show(true); + } else { + complete = function () { + label.hide(); + }; + } + + // Animate or set the opacity + label.alignAttr.opacity = newOpacity; + label[label.isOld ? 'animate' : 'attr'](label.alignAttr, null, complete); + + } + label.isOld = true; + } + }); + }; + }(Highcharts)); + /** + * TrackerMixin for points and graphs + */ + + var TrackerMixin = Highcharts.TrackerMixin = { + + drawTrackerPoint: function () { + var series = this, + chart = series.chart, + pointer = chart.pointer, + cursor = series.options.cursor, + css = cursor && { cursor: cursor }, + onMouseOver = function (e) { + var target = e.target, + point; + + while (target && !point) { + point = target.point; + target = target.parentNode; + } + + if (point !== UNDEFINED && point !== chart.hoverPoint) { // undefined on graph in scatterchart + point.onMouseOver(e); + } + }; + + // Add reference to the point + each(series.points, function (point) { + if (point.graphic) { + point.graphic.element.point = point; + } + if (point.dataLabel) { + point.dataLabel.element.point = point; + } + }); + + // Add the event listeners, we need to do this only once + if (!series._hasTracking) { + each(series.trackerGroups, function (key) { + if (series[key]) { // we don't always have dataLabelsGroup + series[key] + .addClass(PREFIX + 'tracker') + .on('mouseover', onMouseOver) + .on('mouseout', function (e) { + pointer.onTrackerMouseOut(e); + }) + .css(css); + if (hasTouch) { + series[key].on('touchstart', onMouseOver); + } + } + }); + series._hasTracking = true; + } + }, + + /** + * 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. + */ + drawTrackerGraph: function () { + var series = this, + options = series.options, + trackByArea = options.trackByArea, + trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath), + trackerPathLength = trackerPath.length, + chart = series.chart, + pointer = chart.pointer, + renderer = chart.renderer, + snap = chart.options.tooltip.snap, + tracker = series.tracker, + cursor = options.cursor, + css = cursor && { cursor: cursor }, + i, + onMouseOver = function () { + if (chart.hoverSeries !== series) { + series.onMouseOver(); + } + }, + /* + * Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable + * IE6: 0.002 + * IE7: 0.002 + * IE8: 0.002 + * IE9: 0.00000000001 (unlimited) + * IE10: 0.0001 (exporting only) + * FF: 0.00000000001 (unlimited) + * Chrome: 0.000001 + * Safari: 0.000001 + * Opera: 0.00000000001 (unlimited) + */ + TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.0001 : 0.002) + ')'; + + // Extend end points. A better way would be to use round linecaps, + // but those are not clickable in VML. + if (trackerPathLength && !trackByArea) { + 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 = renderer.path(trackerPath) + .attr({ + 'stroke-linejoin': 'round', // #1225 + visibility: series.visible ? VISIBLE : HIDDEN, + stroke: TRACKER_FILL, + fill: trackByArea ? TRACKER_FILL : NONE, + 'stroke-width': options.lineWidth + (trackByArea ? 0 : 2 * snap), + zIndex: 2 + }) + .add(series.group); + + // The tracker is added to the series group, which is clipped, but is covered + // by the marker group. So the marker group also needs to capture events. + each([series.tracker, series.markerGroup], function (tracker) { + tracker.addClass(PREFIX + 'tracker') + .on('mouseover', onMouseOver) + .on('mouseout', function (e) { + pointer.onTrackerMouseOut(e); + }) + .css(css); + + if (hasTouch) { + tracker.on('touchstart', onMouseOver); + } + }); + } + } + }; + /* End TrackerMixin */ + + + /** + * Add tracking event listener to the series group, so the point graphics + * themselves act as trackers + */ + + if (seriesTypes.column) { + ColumnSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint; + } + + if (seriesTypes.pie) { + seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint; + } + + if (seriesTypes.scatter) { + ScatterSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint; + } + + /* + * Extend Legend for item events + */ + extend(Legend.prototype, { + + setItemEvents: function (item, legendItem, useHTML, itemStyle, itemHiddenStyle) { + var legend = this; + // Set the events on the item group, or in case of useHTML, the item itself (#1249) + (useHTML ? legendItem : item.legendGroup).on('mouseover', function () { + item.setState(HOVER_STATE); + legendItem.css(legend.options.itemHoverStyle); + }) + .on('mouseout', function () { + legendItem.css(item.visible ? itemStyle : itemHiddenStyle); + item.setState(); + }) + .on('click', function (event) { + var strLegendItemClick = 'legendItemClick', + fnLegendItemClick = function () { + if (item.setVisible) { + item.setVisible(); + } + }; + + // Pass over the click/touch event. #4. + event = { + browserEvent: event + }; + + // click the name or symbol + if (item.firePointEvent) { // point + item.firePointEvent(strLegendItemClick, event, fnLegendItemClick); + } else { + fireEvent(item, strLegendItemClick, event, fnLegendItemClick); + } + }); + }, + + createCheckboxForItem: function (item) { + var legend = this; + + item.checkbox = createElement('input', { + type: 'checkbox', + checked: item.selected, + defaultChecked: item.selected // required by IE7 + }, legend.options.itemCheckboxStyle, legend.chart.container); + + addEvent(item.checkbox, 'click', function (event) { + var target = event.target; + fireEvent( + item.series || item, + 'checkboxClick', + { // #3712 + checked: target.checked, + item: item + }, + function () { + item.select(); + } + ); + }); + } + }); + + /* + * Add pointer cursor to legend itemstyle in defaultOptions + */ + defaultOptions.legend.itemStyle.cursor = 'pointer'; + + + /* + * Extend the Chart object with interaction + */ + + extend(Chart.prototype, { + /** + * Display the zoom button + */ + showResetZoom: function () { + var chart = this, + lang = defaultOptions.lang, + btnOptions = chart.options.chart.resetZoomButton, + theme = btnOptions.theme, + states = theme.states, + alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox'; + + function zoomOut() { + chart.zoomOut(); + } + + this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, zoomOut, theme, states && states.hover) + .attr({ + align: btnOptions.position.align, + title: lang.resetZoomTitle + }) + .add() + .align(btnOptions.position, false, alignTo); + + }, + + /** + * Zoom out to 1:1 + */ + zoomOut: function () { + var chart = this; + fireEvent(chart, 'selection', { resetSelection: true }, function () { + chart.zoom(); + }); + }, + + /** + * Zoom into a given portion of the chart given by axis coordinates + * @param {Object} event + */ + zoom: function (event) { + var chart = this, + hasZoomed, + pointer = chart.pointer, + displayButton = false, + resetZoomButton; + + // If zoom is called with no arguments, reset the axes + if (!event || event.resetSelection) { + each(chart.axes, function (axis) { + hasZoomed = axis.zoom(); + }); + } else { // else, zoom in on all axes + each(event.xAxis.concat(event.yAxis), function (axisData) { + var axis = axisData.axis, + isXAxis = axis.isXAxis; + + // don't zoom more than minRange + if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) { + hasZoomed = axis.zoom(axisData.min, axisData.max); + if (axis.displayBtn) { + displayButton = true; + } + } + }); + } + + // Show or hide the Reset zoom button + resetZoomButton = chart.resetZoomButton; + if (displayButton && !resetZoomButton) { + chart.showResetZoom(); + } else if (!displayButton && isObject(resetZoomButton)) { + chart.resetZoomButton = resetZoomButton.destroy(); + } + + + // Redraw + if (hasZoomed) { + chart.redraw( + pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation + ); + } + }, + + /** + * Pan the chart by dragging the mouse across the pane. This function is called + * on mouse move, and the distance to pan is computed from chartX compared to + * the first chartX position in the dragging operation. + */ + pan: function (e, panning) { + + var chart = this, + hoverPoints = chart.hoverPoints, + doRedraw; + + // remove active points for shared tooltip + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + each(panning === 'xy' ? [1, 0] : [1], function (isX) { // xy is used in maps + var axis = chart[isX ? 'xAxis' : 'yAxis'][0], + horiz = axis.horiz, + mousePos = e[horiz ? 'chartX' : 'chartY'], + mouseDown = horiz ? 'mouseDownX' : 'mouseDownY', + startPos = chart[mouseDown], + halfPointRange = (axis.pointRange || 0) / 2, + extremes = axis.getExtremes(), + newMin = axis.toValue(startPos - mousePos, true) + halfPointRange, + newMax = axis.toValue(startPos + axis.len - mousePos, true) - halfPointRange, + goingLeft = startPos > mousePos; // #3613 + + if (axis.series.length && + (goingLeft || newMin > mathMin(extremes.dataMin, extremes.min)) && + (!goingLeft || newMax < mathMax(extremes.dataMax, extremes.max))) { + axis.setExtremes(newMin, newMax, false, false, { trigger: 'pan' }); + doRedraw = true; + } + + chart[mouseDown] = mousePos; // set new reference for next run + }); + + if (doRedraw) { + chart.redraw(false); + } + css(chart.container, { cursor: 'move' }); + } + }); + + /* + * Extend the Point object with interaction + */ + extend(Point.prototype, { + /** + * 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 default handler + point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () { + point.selected = point.options.selected = selected; + series.options.data[inArray(point, series.data)] = point.options; + + 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 = loopPoint.options.selected = false; + series.options.data[inArray(loopPoint, series.data)] = loopPoint.options; + loopPoint.setState(NORMAL_STATE); + loopPoint.firePointEvent('unselect'); + } + }); + } + }); + }, + + /** + * Runs on mouse over the point + * + * @param {Object} e The event arguments + * @param {Boolean} byProximity Falsy for kd points that are closest to the mouse, or to + * actually hovered points. True for other points in shared tooltip. + */ + onMouseOver: function (e, byProximity) { + var point = this, + series = point.series, + chart = series.chart, + tooltip = chart.tooltip, + hoverPoint = chart.hoverPoint; + + if (chart.hoverSeries !== series) { + series.onMouseOver(); + } + + // set normal state to previous series + if (hoverPoint && hoverPoint !== point) { + hoverPoint.onMouseOut(); + } + + if (point.series) { // It may have been destroyed, #4130 + + // trigger the event + point.firePointEvent('mouseOver'); + + // update the tooltip + if (tooltip && (!tooltip.shared || series.noSharedTooltip)) { + tooltip.refresh(point, e); + } + + // hover this + point.setState(HOVER_STATE); + if (!byProximity) { + chart.hoverPoint = point; + } + } + }, + + /** + * Runs on mouse out from the point + */ + onMouseOut: function () { + var chart = this.series.chart, + hoverPoints = chart.hoverPoints; + + this.firePointEvent('mouseOut'); + + if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887, #2240 + this.setState(); + chart.hoverPoint = null; + } + }, + + /** + * 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, move) { + var point = this, + plotX = mathFloor(point.plotX), // #4586 + plotY = point.plotY, + 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, + pointMarker = point.marker || {}, + chart = series.chart, + radius, + halo = series.halo, + haloOptions, + newSymbol, + pointAttr; + + state = state || NORMAL_STATE; // empty string + pointAttr = point.pointAttr[state] || series.pointAttr[state]; + + if ( + // already has this state + (state === point.state && !move) || + // selected points don't respond to hover + (point.selected && state !== SELECT_STATE) || + // series' state options is disabled + (stateOptions[state] && stateOptions[state].enabled === false) || + // general point marker's state options is disabled + (state && (stateDisabled || (normalDisabled && markerStateOptions.enabled === false))) || + // individual point marker's state options is disabled + (state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610 + + ) { + return; + } + + // apply hover styles to the existing point + if (point.graphic) { + radius = markerOptions && point.graphic.symbolName && pointAttr.r; + point.graphic.attr(merge( + pointAttr, + radius ? { // new symbol attributes (#507, #612) + x: plotX - radius, + y: plotY - radius, + width: 2 * radius, + height: 2 * radius + } : {} + )); + + // Zooming in from a range with no markers to a range with markers + if (stateMarkerGraphic) { + stateMarkerGraphic.hide(); + } + } else { + // if a graphic is not applied to each point in the normal state, create a shared + // graphic for the hover state + if (state && markerStateOptions) { + radius = markerStateOptions.radius; + newSymbol = pointMarker.symbol || series.symbol; + + // If the point has another symbol than the previous one, throw away the + // state marker graphic and force a new one (#1459) + if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) { + stateMarkerGraphic = stateMarkerGraphic.destroy(); + } + + // Add a new state marker graphic + if (!stateMarkerGraphic) { + if (newSymbol) { + series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol( + newSymbol, + plotX - radius, + plotY - radius, + 2 * radius, + 2 * radius + ) + .attr(pointAttr) + .add(series.markerGroup); + stateMarkerGraphic.currentSymbol = newSymbol; + } + + // Move the existing graphic + } else { + stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054 + x: plotX - radius, + y: plotY - radius + }); + } + } + + if (stateMarkerGraphic) { + stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450 + stateMarkerGraphic.element.point = point; // #4310 + } + } + + // Show me your halo + haloOptions = stateOptions[state] && stateOptions[state].halo; + if (haloOptions && haloOptions.size) { + if (!halo) { + series.halo = halo = chart.renderer.path() + .add(chart.seriesGroup); + } + halo.attr(extend({ + 'fill': point.color || series.color, + 'fill-opacity': haloOptions.opacity, + 'zIndex': -1 // #4929, IE8 added halo above everything + }, + haloOptions.attributes))[move ? 'animate' : 'attr']({ + d: point.haloPath(haloOptions.size) + }); + } else if (halo) { + halo.attr({ d: [] }); + } + + point.state = state; + }, + + /** + * Get the circular path definition for the halo + * @param {Number} size The radius of the circular halo + * @returns {Array} The path definition + */ + haloPath: function (size) { + var series = this.series, + chart = series.chart, + plotBox = series.getPlotBox(), + inverted = chart.inverted, + plotX = Math.floor(this.plotX); + + return chart.renderer.symbols.circle( + plotBox.translateX + (inverted ? series.yAxis.len - this.plotY : plotX) - size, + plotBox.translateY + (inverted ? series.xAxis.len - plotX : this.plotY) - size, + size * 2, + size * 2 + ); + } + }); + + /* + * Extend the Series object with interaction + */ + + extend(Series.prototype, { + /** + * Series mouse over handler + */ + onMouseOver: function () { + var series = this, + chart = series.chart, + hoverSeries = chart.hoverSeries; + + // 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'); + } + + // 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; + + chart.hoverSeries = null; // #182, set to null before the mouseOut event fires + + // 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.shared || series.noSharedTooltip)) { + tooltip.hide(); + } + + // set normal state + series.setState(); + }, + + /** + * Set the state of the graph + */ + setState: function (state) { + var series = this, + options = series.options, + graph = series.graph, + stateOptions = options.states, + lineWidth = options.lineWidth, + attribs, + i = 0; + + 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 + (stateOptions[state].lineWidthPlus || 0); // #4035 + } + + if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML + attribs = { + 'stroke-width': lineWidth + }; + // use attr because animate will cause any other animation on the graph to stop + graph.attr(attribs); + while (series['zoneGraph' + i]) { + series['zoneGraph' + i].attr(attribs); + i = i + 1; + } + } + } + }, + + /** + * 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, + showOrHide, + ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries, + oldVisibility = series.visible; + + // if called without an argument, toggle visibility + series.visible = vis = series.userOptions.visible = vis === UNDEFINED ? !oldVisibility : vis; + showOrHide = vis ? 'show' : 'hide'; + + // show or hide elements + each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function (key) { + if (series[key]) { + series[key][showOrHide](); + } + }); + + + // hide tooltip (#1361) + if (chart.hoverSeries === series || (chart.hoverPoint && chart.hoverPoint.series) === series) { + series.onMouseOut(); + } + + + 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; + } + }); + } + + // show or hide linked series + each(series.linkedSeries, function (otherSeries) { + otherSeries.setVisible(vis, false); + }); + + 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'); + }, + + drawTracker: TrackerMixin.drawTrackerGraph + }); + + // global variables + extend(Highcharts, { + + // Constructors + Color: Color, + Point: Point, + Tick: Tick, + Renderer: Renderer, + SVGElement: SVGElement, + SVGRenderer: SVGRenderer, + + // Various + arrayMin: arrayMin, + arrayMax: arrayMax, + charts: charts, + correctFloat: correctFloat, + dateFormat: dateFormat, + error: error, + format: format, + pathAnim: pathAnim, + getOptions: getOptions, + hasBidiBug: hasBidiBug, + isTouchDevice: isTouchDevice, + setOptions: setOptions, + addEvent: addEvent, + removeEvent: removeEvent, + createElement: createElement, + discardElement: discardElement, + css: css, + each: each, + map: map, + merge: merge, + splat: splat, + stableSort: stableSort, + extendClass: extendClass, + pInt: pInt, + svg: hasSVG, + canvas: useCanVG, + vml: !hasSVG && !useCanVG, + product: PRODUCT, + version: VERSION + }); + + return Highcharts; +})); diff --git a/src/main/resources/nl/topicus/whighcharts/components/jquery.highcharts.min.js b/src/main/resources/nl/topicus/whighcharts/components/jquery.highcharts.min.js deleted file mode 100644 index 78b17ae..0000000 --- a/src/main/resources/nl/topicus/whighcharts/components/jquery.highcharts.min.js +++ /dev/null @@ -1,239 +0,0 @@ -/* - Highcharts JS v2.2.5 (2012-06-08) - - (c) 2009-2011 Torstein H?nsi - - License: www.highcharts.com/license -*/ -(function(){function u(a,b){var c;a||(a={});for(c in b)a[c]=b[c];return a}function la(){for(var a=0,b=arguments,c=b.length,d={};a-1?b.split(".")[1].length:0):a=isNaN(b=M(b))?2:b;var b=a,c=c===void 0?e.decimalPoint:c,d=d===void 0?e.thousandsSep:d,e=f<0?"-":"",a=String(w(f=M(+f||0).toFixed(b))),g=a.length>3?a.length%3:0;return e+(g?a.substr(0,g)+d:"")+ -a.substr(g).replace(/(\d{3})(?=\d)/g,"$1"+d)+(b?c+M(f-a).toFixed(b).slice(2):"")}function sa(a,b){return Array((b||2)+1-String(a).length).join(0)+a}function gb(a,b,c,d){var e,c=o(c,1);e=a/c;b||(b=[1,2,2.5,5,10],d&&d.allowDecimals===!1&&(c===1?b=[1,2,5,10]:c<=0.1&&(b=[1/c])));for(d=0;d=D[hb]&&(i.setMilliseconds(0),i.setSeconds(b>=D[Ya]?0:j*W(i.getSeconds()/j)));if(b>=D[Ya])i[wb](b>=D[Ma]?0:j*W(i[ib]()/j)); -if(b>=D[Ma])i[xb](b>=D[oa]?0:j*W(i[jb]()/j));if(b>=D[oa])i[kb](b>=D[Na]?1:j*W(i[Oa]()/j));b>=D[Na]&&(i[yb](b>=D[ta]?0:j*W(i[$a]()/j)),h=i[ab]());b>=D[ta]&&(h-=h%j,i[zb](h));if(b===D[Za])i[kb](i[Oa]()-i[lb]()+o(d,1));d=1;h=i[ab]();for(var k=i.getTime(),l=i[$a](),m=i[Oa](),i=g?0:(864E5+i.getTimezoneOffset()*6E4)%864E5;kc&&(c=a[b]);return c}function Ba(a,b){for(var c in a)a[c]&&a[c]!==b&&a[c].destroy&&a[c].destroy(),delete a[c]}function Qa(a){cb||(cb=S(ia)); -a&&cb.appendChild(a);cb.innerHTML=""}function mb(a,b){var c="Highcharts error #"+a+": www.highcharts.com/errors/"+a;if(b)throw c;else N.console&&console.log(c)}function ja(a){return parseFloat(a.toPrecision(14))}function ua(a,b){Ra=o(a,b.animation)}function Bb(){var a=V.global.useUTC,b=a?"getUTC":"get",c=a?"setUTC":"set";bb=a?Date.UTC:function(a,b,c,g,h,i){return(new Date(a,b,o(c,1),o(g,0),o(h,0),o(i,0))).getTime()};ib=b+"Minutes";jb=b+"Hours";lb=b+"Day";Oa=b+"Date";$a=b+"Month";ab=b+"FullYear";wb= -c+"Minutes";xb=c+"Hours";kb=c+"Date";yb=c+"Month";zb=c+"FullYear"}function va(){}function Sa(a,b,c){this.axis=a;this.pos=b;this.type=c||"";this.isNew=!0;c||this.addLabel()}function nb(a,b){this.axis=a;if(b)this.options=b,this.id=b.id;return this}function Cb(a,b,c,d,e){var f=a.chart.inverted;this.axis=a;this.isNegative=c;this.options=b;this.x=d;this.stack=e;this.alignOptions={align:b.align||(f?c?"left":"right":"center"),verticalAlign:b.verticalAlign||(f?"middle":c?"bottom":"top"),y:o(b.y,f?4:c?14: --6),x:o(b.x,f?c?-6:6:0)};this.textAlign=b.textAlign||(f?c?"right":"left":"center")}function ob(){this.init.apply(this,arguments)}function pb(a,b){var c=b.borderWidth,d=b.style,e=b.shared,f=w(d.padding);this.chart=a;this.options=b;d.padding=0;this.crosshairs=[];this.currentY=this.currentX=0;this.tooltipIsHidden=!0;this.label=a.renderer.label("",0,0,null,null,null,b.useHTML,null,"tooltip").attr({padding:f,fill:b.backgroundColor,"stroke-width":c,r:b.borderRadius,zIndex:8}).css(d).hide().add();ga||this.label.shadow(b.shadow); -this.shared=e}function Db(a,b){var c=ga?"":b.chart.zoomType;this.zoomX=/x/.test(c);this.zoomY=/y/.test(c);this.options=b;this.chart=a;this.init(a,b.tooltip)}function qb(a){this.init(a)}function rb(a,b){var c,d=a.series;a.series=null;c=C(V,a);c.series=a.series=d;var d=c.chart,e=d.margin,e=aa(e)?e:[e,e,e,e];this.optionsMarginTop=o(d.marginTop,e[0]);this.optionsMarginRight=o(d.marginRight,e[1]);this.optionsMarginBottom=o(d.marginBottom,e[2]);this.optionsMarginLeft=o(d.marginLeft,e[3]);this.runChartClick= -(e=d.events)&&!!e.click;this.callback=b;this.isResizing=0;this.options=c;this.axes=[];this.series=[];this.hasCartesianSeries=d.showAxes;this.init(e)}var A,B=document,N=window,L=Math,t=L.round,W=L.floor,wa=L.ceil,x=L.max,O=L.min,M=L.abs,X=L.cos,da=L.sin,xa=L.PI,Eb=xa*2/360,ya=navigator.userAgent,La=/msie/i.test(ya)&&!N.opera,Ca=B.documentMode===8,Fb=/AppleWebKit/.test(ya),Gb=/Firefox/.test(ya),Da=!!B.createElementNS&&!!B.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect,Rb=Gb&&parseInt(ya.split("Firefox/")[1], -10)<4,ga=!Da&&!La&&!!B.createElement("canvas").getContext,Ta,ea=B.documentElement.ontouchstart!==A,Hb={},sb=0,cb,V,db,Ra,Ua,D,Sb=function(){},ia="div",U="none",tb="rgba(192,192,192,"+(Da?1.0E-6:0.0020)+")",vb="millisecond",hb="second",Ya="minute",Ma="hour",oa="day",Za="week",Na="month",ta="year",bb,ib,jb,lb,Oa,$a,ab,wb,xb,kb,yb,zb,Y={};N.Highcharts={};db=function(a,b,c){if(!s(b)||isNaN(b))return"Invalid date";var a=o(a,"%Y-%m-%d %H:%M:%S"),d=new Date(b),e,f=d[jb](),g=d[lb](),h=d[Oa](),i=d[$a](),j= -d[ab](),k=V.lang,l=k.weekdays,b={a:l[g].substr(0,3),A:l[g],d:sa(h),e:h,b:k.shortMonths[i],B:k.months[i],m:sa(i+1),y:j.toString().substr(2,2),Y:j,H:sa(f),I:sa(f%12||12),l:f%12||12,M:sa(d[ib]()),p:f<12?"AM":"PM",P:f<12?"am":"pm",S:sa(d.getSeconds()),L:sa(t(b%1E3),3)};for(e in b)a=a.replace("%"+e,b[e]);return c?a.substr(0,1).toUpperCase()+a.substr(1):a};Ab.prototype={wrapColor:function(a){if(this.color>=a)this.color=0},wrapSymbol:function(a){if(this.symbol>=a)this.symbol=0}};D=la(vb,1,hb,1E3,Ya,6E4, -Ma,36E5,oa,864E5,Za,6048E5,Na,2592E6,ta,31556952E3);Ua={init:function(a,b,c){var b=b||"",d=a.shift,e=b.indexOf("C")>-1,f=e?7:3,g,b=b.split(" "),c=[].concat(c),h,i,j=function(a){for(g=a.length;g--;)a[g]==="M"&&a.splice(g+1,0,a[g+1],a[g+2],a[g+1],a[g+2])};e&&(j(b),j(c));a.isArea&&(h=b.splice(b.length-6,6),i=c.splice(c.length-6,6));if(d<=c.length/f)for(;d--;)c=[].concat(c).splice(0,f).concat(c);a.shift=0;if(b.length)for(a=c.length;b.length{point.key}
',pointFormat:'{series.name}: {point.y}
',shadow:!0,shared:ga,snap:ea?25:10,style:{color:"#333333",fontSize:"12px",padding:"5px",whiteSpace:"nowrap"}},credits:{enabled:!0,text:"Highcharts.com",href:"http://www.highcharts.com",position:{align:"right",x:-10,verticalAlign:"bottom",y:-5}, -style:{cursor:"pointer",color:"#909090",fontSize:"10px"}}};var Z=V.plotOptions,T=Z.line;Bb();var pa=function(a){var b=[],c;(function(a){(c=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/.exec(a))?b=[w(c[1]),w(c[2]),w(c[3]),parseFloat(c[4],10)]:(c=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(a))&&(b=[w(c[1],16),w(c[2],16),w(c[3],16),1])})(a);return{get:function(c){return b&&!isNaN(b[0])?c==="rgb"?"rgb("+b[0]+","+b[1]+","+b[2]+")":c==="a"?b[3]: -"rgba("+b.join(",")+")":a},brighten:function(a){if(Ka(a)&&a!==0){var c;for(c=0;c<3;c++)b[c]+=w(a*255),b[c]<0&&(b[c]=0),b[c]>255&&(b[c]=255)}return this},setOpacity:function(a){b[3]=a;return this}}};va.prototype={init:function(a,b){this.element=b==="span"?S(b):B.createElementNS("http://www.w3.org/2000/svg",b);this.renderer=a;this.attrSetters={}},animate:function(a,b,c){b=o(b,Ra,!0);Fa(this);if(b){b=C(b);if(c)b.complete=c;eb(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,k=this.attrSetters,l=this.shadows,m,p,q=this;ra(a)&&s(b)&&(c=a,a={},a[c]=b);if(ra(a))c=a,h==="circle"?c={x:"cx",y:"cy"}[c]||c:c==="strokeWidth"&&(c="stroke-width"),q=z(g,c)||this[c]||0,c!=="d"&&c!=="visibility"&&(q=parseFloat(q));else for(c in a)if(j=!1,d=a[c],e=k[c]&&k[c](d,c),e!==!1){e!==A&&(d=e);if(c==="d")d&&d.join&&(d=d.join(" ")),/(NaN| {2}|^$)/.test(d)&&(d="M 0 0");else if(c==="x"&&h==="text"){for(e=0;em&&/[ \-]/.test(b.innerText)&&(F(b,{width:m+"px",display:"block",whiteSpace:"normal"}),k=m),m=a.fontMetrics(b.style.fontSize).b,r=p<0&&-k,y=q<0&&-l,fa=p*q<0,r+=q*m*(fa?1-h:h),y-=p*m*(j?fa?h:1-h:1),i&&(r-=k*h*(p<0?-1:1),j&&(y-=l*h*(q<0?-1:1)),F(b,{textAlign:g})),this.xCorr=r,this.yCorr=y;F(b,{left:e+r+"px",top:f+y+"px"});this.cTT=$}}else this.alignOnAdd= -!0},updateTransform:function(){var a=this.translateX||0,b=this.translateY||0,c=this.inverted,d=this.rotation,e=[];c&&(a+=this.attr("width"),b+=this.attr("height"));(a||b)&&e.push("translate("+a+","+b+")");c?e.push("rotate(90) scale(-1,1)"):d&&e.push("rotate("+d+" "+(this.x||0)+" "+(this.y||0)+")");e.length&&z(this.element,"transform",e.join(" "))},toFront:function(){var a=this.element;a.parentNode.appendChild(a);return this},align:function(a,b,c){a?(this.alignOptions=a,this.alignByTranslate=b,c|| -this.renderer.alignedObjects.push(this)):(a=this.alignOptions,b=this.alignByTranslate);var c=o(c,this.renderer),d=a.align,e=a.verticalAlign,f=(c.x||0)+(a.x||0),g=(c.y||0)+(a.y||0),h={};/^(right|center)$/.test(d)&&(f+=(c.width-(a.width||0))/{right:1,center:2}[d]);h[b?"translateX":"x"]=t(f);/^(bottom|middle)$/.test(e)&&(g+=(c.height-(a.height||0))/({bottom:1,middle:2}[e]||1));h[b?"translateY":"y"]=t(g);this[this.placed?"animate":"attr"](h);this.placed=!0;this.alignAttr=h;return this},getBBox:function(a){var b, -c,d=this.rotation;c=this.element;var e=d*Eb;if(c.namespaceURI==="http://www.w3.org/2000/svg"||this.renderer.forExport){try{b=c.getBBox?u({},c.getBBox()):{width:c.offsetWidth,height:c.offsetHeight}}catch(f){}if(!b||b.width<0)b={width:0,height:0};a=b.width;c=b.height;if(d)b.width=M(c*da(e))+M(a*X(e)),b.height=M(c*X(e))+M(a*da(e))}else b=this.htmlGetBBox(a);return b},show:function(){return this.attr({visibility:"visible"})},hide:function(){return this.attr({visibility:"hidden"})},add:function(a){var b= -this.renderer,c=a||b,d=c.element||b.box,e=d.childNodes,f=this.element,g=z(f,"zIndex"),h;this.parentInverted=a&&a.inverted;this.textStr!==void 0&&b.buildText(this);if(g)c.handleZ=!0,g=w(g);if(c.handleZ)for(c=0;cg||!s(g)&&s(b))){d.insertBefore(f,a);h=!0;break}h||d.appendChild(f);this.added=!0;E(this,"add");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=a.box, -e,f;b.onclick=b.onmouseout=b.onmouseover=b.onmousemove=null;Fa(a);if(a.clipPath)a.clipPath=a.clipPath.destroy();if(a.stops){for(f=0;f/g,'').replace(/<(i|em)>/g,'').replace(/
/g,"").split(//g),d=b.childNodes,e=/style="([^"]+)"/,f=/href="([^"]+)"/,g=z(b,"x"),h=a.styles,i=h&&w(h.width),j=h&&h.lineHeight,k,h=d.length,l=[];h--;)b.removeChild(d[h]);i&&!a.added&&this.box.appendChild(b);c[c.length-1]===""&&c.pop();n(c,function(c,d){var h,fa=0,r,c=c.replace(//g,"|||");h=c.split("|||");n(h,function(c){if(c!==""||h.length===1){var m={},n=B.createElementNS("http://www.w3.org/2000/svg", -"tspan");e.test(c)&&z(n,"style",c.match(e)[1].replace(/(;| |^)color([ :])/,"$1fill$2"));f.test(c)&&(z(n,"onclick",'location.href="'+c.match(f)[1]+'"'),F(n,{cursor:"pointer"}));c=(c.replace(/<(.|\n)*?>/g,"")||" ").replace(/</g,"<").replace(/>/g,">");n.appendChild(B.createTextNode(c));fa?m.dx=3:m.x=g;if(!fa){if(d){!Da&&a.renderer.forExport&&F(n,{display:"block"});r=N.getComputedStyle&&w(N.getComputedStyle(k,null).getPropertyValue("line-height"));if(!r||isNaN(r)){var o;if(!(o=j))if(!(o=k.offsetHeight))l[d]= -b.getBBox().height,o=t(l[d]-(l[d-1]||0))||18;r=o}z(n,"dy",r)}k=n}z(n,m);b.appendChild(n);fa++;if(i)for(var c=c.replace(/-/g,"- ").split(" "),H=[];c.length||H.length;)o=a.getBBox().width,m=o>i,!m||c.length===1?(c=H,H=[],c.length&&(n=B.createElementNS("http://www.w3.org/2000/svg","tspan"),z(n,{dy:j||16,x:g}),b.appendChild(n),o>i&&(i=o))):(n.removeChild(n.firstChild),H.unshift(c.pop())),c.length&&n.appendChild(B.createTextNode(c.join(" ").replace(/- /g,"-")))}})})},button:function(a,b,c,d,e,f,g){var h= -this.label(a,b,c),i=0,j,k,l,m,p,a={x1:0,y1:0,x2:0,y2:1},e=C(la("stroke-width",1,"stroke","#999","fill",la("linearGradient",a,"stops",[[0,"#FFF"],[1,"#DDD"]]),"r",3,"padding",3,"style",la("color","black")),e);l=e.style;delete e.style;f=C(e,la("stroke","#68A","fill",la("linearGradient",a,"stops",[[0,"#FFF"],[1,"#ACF"]])),f);m=f.style;delete f.style;g=C(e,la("stroke","#68A","fill",la("linearGradient",a,"stops",[[0,"#9BD"],[1,"#CDF"]])),g);p=g.style;delete g.style;I(h.element,"mouseenter",function(){h.attr(f).css(m)}); -I(h.element,"mouseleave",function(){j=[e,f,g][i];k=[l,m,p][i];h.attr(j).css(k)});h.setState=function(a){(i=a)?a===2&&h.attr(g).css(p):h.attr(e).css(l)};return h.on("click",function(){d.call(h)}).attr(e).css(u({cursor:"default"},l))},crispLine:function(a,b){a[1]===a[4]&&(a[1]=a[4]=t(a[1])+b%2/2);a[2]===a[5]&&(a[2]=a[5]=t(a[2])+b%2/2);return a},path:function(a){var b={fill:U};Ja(a)?b.d=a:aa(a)&&u(b,a);return this.createElement("path").attr(b)},circle:function(a,b,c){a=aa(a)?a:{x:a,y:b,r:c};return this.createElement("circle").attr(a)}, -arc:function(a,b,c,d,e,f){if(aa(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,c||0,{innerR:d||0,start:e||0,end:f||0})},rect:function(a,b,c,d,e,f){e=aa(a)?a.r:e;e=this.createElement("rect").attr({rx:e,ry:e,fill:U});return e.attr(aa(a)?a:e.crisp(f,a,b,x(c,0),x(d,0)))},setSize:function(a,b,c){var d=this.alignedObjects,e=d.length;this.width=a;this.height=b;for(this.boxWrapper[o(c,!0)?"animate":"attr"]({width:a,height:b});e--;)d[e].align()},g:function(a){var b= -this.createElement("g");return s(a)?b.attr({"class":"highcharts-"+a}):b},image:function(a,b,c,d,e){var f={preserveAspectRatio:U};arguments.length>1&&u(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,f){var g,h=this.symbols[a],h=h&&h(t(b),t(c),d,e,f),i=/^url\((.*?)\)$/,j,k;h?(g=this.path(h),u(g,{symbolName:a,x:b,y:c, -width:d,height:e}),f&&u(g,f)):i.test(a)&&(k=function(a,b){a.attr({width:b[0],height:b[1]});a.alignByTranslate||a.translate(-t(b[0]/2),-t(b[1]/2))},j=a.match(i)[1],a=Hb[j],g=this.image(j).attr({x:b,y:c}),a?k(g,a):(g.attr({width:0,height:0}),S("img",{onload:function(){k(g,Hb[j]=[this.width,this.height])},src:j})));return g},symbols:{circle:function(a,b,c,d){var e=0.166*c;return["M",a+c/2,b,"C",a+c+e,b,a+c+e,b+d,a+c/2,b+d,"C",a-e,b+d,a-e,b,a+c/2,b,"Z"]},square:function(a,b,c,d){return["M",a,b,"L",a+ -c,b,a+c,b+d,a,b+d,"Z"]},triangle:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d,a,b+d,"Z"]},"triangle-down":function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c/2,b+d,"Z"]},diamond:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d/2,a+c/2,b+d,a,b+d/2,"Z"]},arc:function(a,b,c,d,e){var f=e.start,c=e.r||c||d,g=e.end-1.0E-6,d=e.innerR,h=e.open,i=X(f),j=da(f),k=X(g),g=da(g),e=e.end-f');if(b)c=b===ia||b==="span"||b==="img"?c.join(""):a.prepVML(c),this.element=S(c);this.renderer=a;this.attrSetters={}},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);Ca&&d.gVis==="hidden"&&F(c,{visibility:"hidden"});d.appendChild(c);this.added=!0;this.alignOnAdd&& -!this.deferUpdateTransform&&this.updateTransform();E(this,"add");return this},toggleChildren:function(a,b){for(var c=a.childNodes,d=c.length;d--;)F(c[d],{visibility:b}),c[d].nodeName==="DIV"&&this.toggleChildren(c[d],b)},updateTransform:va.prototype.htmlUpdateTransform,attr:function(a,b){var c,d,e,f=this.element||{},g=f.style,h=f.nodeName,i=this.renderer,j=this.symbolName,k,l=this.shadows,m,p=this.attrSetters,q=this;ra(a)&&s(b)&&(c=a,a={},a[c]=b);if(ra(a))c=a,q=c==="strokeWidth"||c==="stroke-width"? -this.strokeweight:this[c];else for(c in a)if(d=a[c],m=!1,e=p[c]&&p[c](d,c),e!==!1&&d!==null){e!==A&&(d=e);if(j&&/^(x|y|r|start|end|width|height|innerR|anchorX|anchorY)/.test(c))k||(this.symbolAttr(a),k=!0),m=!0;else if(c==="d"){d=d||[];this.d=d.join(" ");e=d.length;for(m=[];e--;)m[e]=Ka(d[e])?t(d[e]*10)-5:d[e]==="Z"?"x":d[e];d=m.join(" ")||"x";f.path=d;if(l)for(e=l.length;e--;)l[e].path=l[e].cutOff?this.cutOffPath(d,l[e].cutOff):d;m=!0}else if(c==="zIndex"||c==="visibility"){if(Ca&&c==="visibility"&& -h==="DIV")f.gVis=d,this.toggleChildren(f,d),d==="visible"&&(d=null);d&&(g[c]=d);m=!0}else if(c==="width"||c==="height")d=x(0,d),this[c]=d,this.updateClipping?(this[c]=d,this.updateClipping()):g[c]=d,m=!0;else if(c==="x"||c==="y")this[c]=d,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?!0:!1,c="strokeweight",this[c]=d,Ka(d)&&(d+="px");else if(c==="dashstyle")(f.getElementsByTagName("stroke")[0]|| -S(i.prepVML([""]),null,null,f))[c]=d||"solid",this.dashstyle=d,m=!0;else if(c==="fill")h==="SPAN"?g.color=d:(f.filled=d!==U?!0:!1,d=i.color(d,f,c),c="fillcolor");else if(h==="shape"&&c==="rotation")this[c]=d;else if(c==="translateX"||c==="translateY"||c==="rotation")this[c]=d,this.updateTransform(),m=!0;else if(c==="text")this.bBox=null,f.innerHTML=d,m=!0;if(l&&c==="visibility")for(e=l.length;e--;)l[e].style[c]=d;m||(Ca?f[c]=d:z(f,c,d))}return q},clip:function(a){var b=this,c=a.members,d= -b.element,e=d.parentNode;c.push(b);b.destroyClip=function(){za(c,b)};e&&e.className==="highcharts-tracker"&&!Ca&&F(d,{visibility:"hidden"});return b.css(a.getCSS(b))},css:va.prototype.htmlCss,safeRemoveChild:function(a){a.parentNode&&Qa(a)},destroy:function(){this.destroyClip&&this.destroyClip();return va.prototype.destroy.apply(this)},empty:function(){for(var a=this.element.childNodes,b=a.length,c;b--;)c=a[b],c.parentNode.removeChild(c)},on:function(a,b){this.element["on"+a]=function(){var a=N.event; -a.target=a.srcElement;b(a)};return this},cutOffPath:function(a,b){var c,a=a.split(/[ ,]/);c=a.length;if(c===9||c===11)a[c-4]=a[c-2]=w(a[c-2])-10*b;return a.join(" ")},shadow:function(a,b,c){var d=[],e=this.element,f=this.renderer,g,h=e.style,i,j=e.path,k,l;j&&typeof j.value!=="string"&&(j="x");l=j;if(a){for(a=1;a<=3;a++){k=7-2*a;c&&(l=this.cutOffPath(j.value,k+0.5));i=[''];g= -S(f.prepVML(i),null,{left:w(h.left)+1,top:w(h.top)+1});if(c)g.cutOff=k+1;i=[''];S(f.prepVML(i),null,null,g);b?b.element.appendChild(g):e.parentNode.insertBefore(g,e);d.push(g)}this.shadows=d}return this}};ka=ca(va,ka);var ha={Element:ka,isIE8:ya.indexOf("MSIE 8.0")>-1,init:function(a,b,c){var d,e;this.alignedObjects=[];d=this.createElement(ia);e=d.element;e.style.position="relative";a.appendChild(d.element);this.box=e;this.boxWrapper=d;this.setSize(b,c, -!1);if(!B.namespaces.hcv)B.namespaces.add("hcv","urn:schemas-microsoft-com:vml"),B.createStyleSheet().cssText="hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke{ behavior:url(#default#VML); display: inline-block; } "},isHidden:function(){return!this.box.offsetWidth},clipRect:function(a,b,c,d){var e=this.createElement();return u(e,{members:[],left:a,top:b,width:c,height:d,getCSS:function(a){var b=a.inverted,c=this.top,d=this.left,e=d+this.width,k=c+this.height,c={clip:"rect("+t(b?d:c)+"px,"+t(b?k: -e)+"px,"+t(b?e:k)+"px,"+t(b?c:d)+"px)"};!b&&Ca&&a.element.nodeName!=="IMG"&&u(c,{width:e+"px",height:k+"px"});return c},updateClipping:function(){n(e.members,function(a){a.css(e.getCSS(a))})}})},color:function(a,b,c){var d,e=/^rgba/,f,g=U;a&&a.linearGradient?f="gradient":a&&a.radialGradient&&(f="pattern");if(f){var h,i,j=a.linearGradient||a.radialGradient,k,l,m,p,q,o,r="",a=a.stops,y,s=[];l=a[0];y=a[a.length-1];l[0]>0&&a.unshift([0,l[1]]);y[0]<1&&a.push([1,y[1]]);n(a,function(a,b){e.test(a[1])?(d= -pa(a[1]),h=d.get("rgb"),i=d.get("a")):(h=a[1],i=1);s.push(a[0]*100+"% "+h);b?(p=i,q=h):(m=i,o=h)});f==="gradient"?(k=j.x1||j[0]||0,a=j.y1||j[1]||0,l=j.x2||j[2]||0,j=j.y2||j[3]||0,k=90-L.atan((j-a)/(l-k))*180/xa):(g=j.r*2,r='src="http://code.highcharts.com/gfx/radial-gradient.png" size="'+g+","+g+'" origin="0.5,0.5" position="'+j.cx+","+j.cy+'" color2="'+o+'" ',g=q);c==="fill"?(c=[''], -S(this.prepVML(c),null,null,b)):g=h}else if(e.test(a)&&b.tagName!=="IMG")d=pa(a),c=["<",c,' opacity="',d.get("a"),'"/>'],S(this.prepVML(c),null,null,b),g=d.get("rgb");else{b=b.getElementsByTagName(c);if(b.length)b[0].opacity=1;g=a}return g},prepVML:function(a){var b=this.isIE8,a=a.join("");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);')): -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(aa(a))b=a.y,c=a.width,d=a.height,f=a.strokeWidth,a=a.x;var g=this.symbol("rect");g.r=e;return g.attr(g.crisp(f,a,b,x(c,0),x(d,0)))},invertChild:function(a,b){var c=b.style;F(a,{flip:"x",left:w(c.width)-1,top:w(c.height)-1,rotation:-90})},symbols:{arc:function(a,b,c,d,e){var f=e.start,g=e.end,h=e.r||c||d,c=X(f),d=da(f),i=X(g),j=da(g),k=e.innerR,l=0.08/h,m=k&&0.1/k||0;if(g-f===0)return["x"];else 2*xa-g+fj&&(c=!1)):h+k>m&&(h=m-k,d&&h+l0&&b.height>0){f=C({align:c&&k&&"center",x:c?!k&&4:10,verticalAlign:!c&&k&&"middle",y:c?k?16:10:k?6:-4,rotation:c&&!k&&90},f);if(!g)a.label=g=u.text(f.text,0,0).attr({align:f.textAlign||f.align,rotation:f.rotation, -zIndex:y}).css(f.style).add();b=[q[1],q[4],o(q[6],q[1])];q=[q[2],q[5],o(q[7],q[2])];c=Pa(b);k=Pa(q);g.align(f,!1,{x:c,y:k,width:Aa(b)-c,height:Aa(q)-k});g.show()}else g&&g.hide();return a},destroy:function(){za(this.axis.plotLinesAndBands,this);Ba(this,this.axis)}};Cb.prototype={destroy:function(){Ba(this,this.axis)},setTotal:function(a){this.cum=this.total=a},render:function(a){var b=this.options.formatter.call(this);this.label?this.label.attr({text:b,visibility:"hidden"}):this.label=this.axis.chart.renderer.text(b, -0,0).css(this.options.style).attr({align:this.textAlign,rotation:this.options.rotation,visibility:"hidden"}).add(a)},setOffset:function(a,b){var c=this.axis,d=c.chart,e=d.inverted,f=this.isNegative,g=c.translate(this.total,0,0,0,1),c=c.translate(0),c=M(g-c),h=d.xAxis[0].translate(this.x)+a,d=d.plotHeight,e={x:e?f?g:g-c:h,y:e?d-h-b:f?d-g-c:d-g,width:e?c:b,height:e?b:c};this.label&&this.label.align(this.alignOptions,null,e).attr({visibility:"visible"})}};ob.prototype={defaultOptions:{dateTimeLabelFormats:{millisecond:"%H:%M:%S.%L", -second:"%H:%M:%S",minute:"%H:%M",hour:"%H:%M",day:"%e. %b",week:"%e. %b",month:"%b '%y",year:"%Y"},endOnTick:!1,gridLineColor:"#C0C0C0",labels:G,lineColor:"#C0D0E0",lineWidth:1,minPadding:0.01,maxPadding:0.01,minorGridLineColor:"#E0E0E0",minorGridLineWidth:1,minorTickColor:"#A0A0A0",minorTickLength:2,minorTickPosition:"outside",startOfWeek:1,startOnTick:!1,tickColor:"#C0D0E0",tickLength:5,tickmarkPlacement:"between",tickPixelInterval:100,tickPosition:"outside",tickWidth:1,title:{align:"middle",style:{color:"#6D869F", -fontWeight:"bold"}},type:"linear"},defaultYAxisOptions:{endOnTick:!0,gridLineWidth:1,tickPixelInterval:72,showLastLabel:!0,labels:{align:"right",x:-8,y:3},lineWidth:0,maxPadding:0.05,minPadding:0.05,startOnTick:!0,tickWidth:0,title:{rotation:270,text:"Y-values"},stackLabels:{enabled:!1,formatter:function(){return this.total},style:G.style}},defaultLeftAxisOptions:{labels:{align:"right",x:-8,y:null},title:{rotation:270}},defaultRightAxisOptions:{labels:{align:"left",x:8,y:null},title:{rotation:90}}, -defaultBottomAxisOptions:{labels:{align:"center",x:0,y:14},title:{rotation:0}},defaultTopAxisOptions:{labels:{align:"center",x:0,y:-5},title:{rotation:0}},init:function(a,b){var c=b.isX;this.horiz=a.inverted?!c:c;this.xOrY=(this.isXAxis=c)?"x":"y";this.opposite=b.opposite;this.side=this.horiz?this.opposite?0:2:this.opposite?1:3;this.setOptions(b);var d=this.options,e=d.type,f=e==="datetime";this.labelFormatter=d.labels.formatter||this.defaultLabelFormatter;this.staggerLines=this.horiz&&d.labels.staggerLines; -this.userOptions=b;this.minPixelPadding=0;this.chart=a;this.reversed=d.reversed;this.categories=d.categories;this.isLog=e==="logarithmic";this.isLinked=s(d.linkedTo);this.isDatetimeAxis=f;this.ticks={};this.minorTicks={};this.plotLinesAndBands=[];this.alternateBands={};this.len=0;this.minRange=this.userMinRange=d.minRange||d.maxZoom;this.range=d.range;this.offset=d.offset||0;this.stacks={};this.min=this.max=null;var g,d=this.options.events;a.axes.push(this);a[c?"xAxis":"yAxis"].push(this);this.series= -[];if(a.inverted&&c&&this.reversed===A)this.reversed=!0;this.removePlotLine=this.removePlotBand=this.removePlotBandOrLine;this.addPlotLine=this.addPlotBand=this.addPlotBandOrLine;for(g in d)I(this,g,d[g]);if(this.isLog)this.val2lin=ma,this.lin2val=ba},setOptions:function(a){this.options=C(this.defaultOptions,this.isXAxis?{}:this.defaultYAxisOptions,[this.defaultTopAxisOptions,this.defaultRightAxisOptions,this.defaultBottomAxisOptions,this.defaultLeftAxisOptions][this.side],a)},defaultLabelFormatter:function(){var a= -this.axis,b=this.value,c=a.tickInterval,d=this.dateTimeLabelFormat;return a.categories?b:d?db(d,b):c%1E6===0?b/1E6+"M":c%1E3===0?b/1E3+"k":b>=1E3?Xa(b,0):Xa(b,-1)},getSeriesExtremes:function(){var a=this,b=a.chart,c=a.stacks,d=[],e=[],f;a.dataMin=a.dataMax=null;n(a.series,function(g){if(g.visible||!b.options.chart.ignoreHiddenSeries){var h=g.options,i,j,k,l,m,p,q,n,r,y=h.threshold,t,u=[],v=0;if(a.isLog&&y<=0)y=h.threshold=null;if(a.isXAxis){if(h=g.xData,h.length)a.dataMin=O(o(a.dataMin,h[0]),Pa(h)), -a.dataMax=x(o(a.dataMax,h[0]),Aa(h))}else{var H,J,K,C=g.cropped,Ha=g.xAxis.getExtremes(),w=!!g.modifyValue;i=h.stacking;a.usePercentage=i==="percent";if(i)m=h.stack,l=g.type+o(m,""),p="-"+l,g.stackKey=l,j=d[l]||[],d[l]=j,k=e[p]||[],e[p]=k;if(a.usePercentage)a.dataMin=0,a.dataMax=99;h=g.processedXData;q=g.processedYData;t=q.length;for(f=0;f=Ha.min&&(h[f-1]||n)<=Ha.max))if(n=r.length)for(;n--;)r[n]!==null&&(u[v++]=r[n]);else u[v++]=r;if(!a.usePercentage&&u.length)a.dataMin=O(o(a.dataMin,u[0]),Pa(u)),a.dataMax=x(o(a.dataMax,u[0]),Aa(u));if(s(y))if(a.dataMin>=y)a.dataMin=y,a.ignoreMinPadding=!0;else if(a.dataMaxe+this.width)l= -!0}else if(c=e,h=k-this.right,gf+this.height)l=!0;return l?null:d.renderer.crispLine(["M",c,g,"L",h,i],b||0)},getPlotBandPath:function(a,b){var c=this.getPlotLinePath(b),d=this.getPlotLinePath(a);d&&c?d.push(c[4],c[5],c[1],c[2]):d=null;return d},getLinearTickPositions:function(a,b,c){for(var d,b=ja(W(b/a)*a),c=ja(wa(c/a)*a),e=[];b<=c;){e.push(b);b=ja(b+a);if(b===d)break;d=b}return e},getLogTickPositions:function(a,b,c,d){var e=this.options,f=this.len,g=[];if(!d)this._minorAutoInterval=null; -if(a>=0.5)a=t(a),g=this.getLinearTickPositions(a,b,c);else if(a>=0.08)for(var f=W(b),h,i,j,k,l,e=a>0.3?[1,2,4]:a>0.15?[1,2,4,6,8]:[1,2,3,4,5,6,7,8,9];fb&&g.push(k),k>c&&(l=!0),k=j}else if(b=ba(b),c=ba(c),a=e[d?"minorTickInterval":"tickInterval"],a=o(a==="auto"?null:a,this._minorAutoInterval,(c-b)*(e.tickPixelInterval/(d?5:1))/((d?f/this.tickPositions.length:f)||1)),a=gb(a,null,L.pow(10,W(L.log(a)/L.LN10))),g=Ea(this.getLinearTickPositions(a, -b,c),ma),!d)this._minorAutoInterval=a/5;if(!d)this.tickInterval=a;return g},getMinorTickPositions:function(){var a=this.tickPositions,b=this.minorTickInterval,c=[],d,e;if(this.isLog){e=a.length;for(d=1;d=this.minRange,f,g,h,i,j;if(this.isXAxis&&this.minRange===A&& -!this.isLog)s(a.min)||s(a.max)?this.minRange=null:(n(this.series,function(a){i=a.xData;for(g=j=a.xIncrement?1:i.length-1;g>0;g--)if(h=i[g]-i[g-1],f===A||h0||!b.ignoreMaxPadding))b.max+=c*j}b.tickInterval=b.min===b.max||b.min===void 0||b.max===void 0?1:h&&!l&&m===b.linkedParent.options.tickPixelInterval?b.linkedParent.tickInterval:o(l,p?1:(b.max-b.min)*m/(b.len||1));g&&!a&&n(b.series,function(a){a.processData(b.min!==b.oldMin||b.max!==b.oldMax)}); -b.setAxisTranslation();b.beforeSetTickPositions&&b.beforeSetTickPositions();if(b.postProcessTickInterval)b.tickInterval=b.postProcessTickInterval(b.tickInterval);if(!f&&!e&&(a=L.pow(10,W(L.log(b.tickInterval)/L.LN10)),!s(d.tickInterval)))b.tickInterval=gb(b.tickInterval,null,a,d);b.minorTickInterval=d.minorTickInterval==="auto"&&b.tickInterval?b.tickInterval/5:d.minorTickInterval;b.tickPositions=i=d.tickPositions||i&&i.apply(b,[b.min,b.max]);if(!i)i=f?(b.getNonLinearTimeTicks||Pb)(Ob(b.tickInterval, -d.units),b.min,b.max,d.startOfWeek,b.ordinalPositions,b.closestPointRange,!0):e?b.getLogTickPositions(b.tickInterval,b.min,b.max):b.getLinearTickPositions(b.tickInterval,b.min,b.max),b.tickPositions=i;if(!h)e=i[0],f=i[i.length-1],d.startOnTick?b.min=e:b.min>e&&i.shift(),d.endOnTick?b.max=f:b.maxb[d]&&this.options.alignTicks!==!1)b[d]= -c.length;a.maxTicks=b},adjustTickAmount:function(){var a=this.xOrY,b=this.tickPositions,c=this.chart.maxTicks;if(c&&c[a]&&!this.isDatetimeAxis&&!this.categories&&!this.isLinked&&this.options.alignTicks!==!1){var d=this.tickAmount,e=b.length;this.tickAmount=a=c[a];if(ea||a===null?a=c:b=a.min&&b<=a.max)j[b]||(j[b]=new Sa(a,b)),r&&j[b].isNew&&j[b].render(c,!0),j[b].isActive=!0,j[b].render(c)}),p&&n(g,function(b,c){if(c%2===0&&b1|| -M(b-c.currentY)>1?function(){c.move(a,b)}:null},hide:function(){if(!this.tooltipIsHidden){var a=this.chart.hoverPoints;this.label.hide();a&&n(a,function(a){a.setState()});this.chart.hoverPoints=null;this.tooltipIsHidden=!0}},hideCrosshairs:function(){n(this.crosshairs,function(a){a&&a.hide()})},getAnchor:function(a,b){var c,d=this.chart,e=d.inverted,f=0,g=0,a=na(a);c=a[0].tooltipPos;c||(n(a,function(a){f+=a.plotX;g+=a.plotLow?(a.plotLow+a.plotHigh)/2:a.plotY}),f/=a.length,g/=a.length,c=[e?d.plotWidth- -g:f,this.shared&&!e&&a.length>1&&b?b.chartY-d.plotTop:e?d.plotHeight-f:g]);return Ea(c,t)},getPosition:function(a,b,c){var d=this.chart,e=d.plotLeft,f=d.plotTop,g=d.plotWidth,h=d.plotHeight,i=o(this.options.distance,12),j=c.plotX,c=c.plotY,d=j+e+(d.inverted?i:-a-i),k=c-b+f+15,l;d<7&&(d=e+j+i);d+a>e+g&&(d-=d+a-(e+g),k=c-b+f-i,l=!0);k=k&&c<=k+b&&(k=c+f+i));k+b>f+h&&(k=x(f,f+h-b-i));return{x:d,y:k}},refresh:function(a,b){function c(){var a=this.points||na(this),b=a[0].series,c;c=[b.tooltipHeaderFormatter(a[0].key)]; -n(a,function(a){b=a.series;c.push(b.tooltipFormatter&&b.tooltipFormatter(a)||a.point.tooltipFormatter(b.tooltipOptions.pointFormat))});c.push(f.footerFormat||"");return c.join("")}var d=this.chart,e=this.label,f=this.options,g,h,i,j={},k,l=[];k=f.formatter||c;var j=d.hoverPoints,m,p=f.crosshairs;i=this.shared;h=this.getAnchor(a,b);g=h[0];h=h[1];i&&(!a.series||!a.series.noSharedTooltip)?(j&&n(j,function(a){a.setState()}),d.hoverPoints=a,n(a,function(a){a.setState("hover");l.push(a.getLabelConfig())}), -j={x:a[0].category,y:a[0].y},j.points=l,a=a[0]):j=a.getLabelConfig();k=k.call(j);j=a.series;i=i||!j.isCartesian||j.tooltipOutsidePlot||d.isInsidePlot(g,h);k===!1||!i?this.hide():(this.tooltipIsHidden&&e.show(),e.attr({text:k}),m=f.borderColor||a.color||j.color||"#606060",e.attr({stroke:m}),e=(f.positioner||this.getPosition).call(this,e.width,e.height,{plotX:g,plotY:h}),this.move(t(e.x),t(e.y)),this.tooltipIsHidden=!1);if(p){p=na(p);for(e=p.length;e--;)if(i=a.series[e?"yAxis":"xAxis"],p[e]&&i)if(i= -i.getPlotLinePath(e?o(a.stackY,a.y):a.x,1),this.crosshairs[e])this.crosshairs[e].attr({d:i,visibility:"visible"});else{j={"stroke-width":p[e].width||1,stroke:p[e].color||"#C0C0C0",zIndex:p[e].zIndex||2};if(p[e].dashStyle)j.dashstyle=p[e].dashStyle;this.crosshairs[e]=d.renderer.path(i).attr(j).add()}}E(d,"tooltipRefresh",{text:k,x:g+d.plotLeft,y:h+d.plotTop,borderColor:m})},tick:function(){this.tooltipTick&&this.tooltipTick()}};Db.prototype={normalizeMouseEvent:function(a){var b,c,d,a=a||N.event;if(!a.target)a.target= -a.srcElement;if(a.originalEvent)a=a.originalEvent;if(a.event)a=a.event;d=a.touches?a.touches.item(0):a;this.chartPosition=b=Jb(this.chart.container);d.pageX===A?(c=a.x,b=a.y):(c=d.pageX-b.left,b=d.pageY-b.top);return u(a,{chartX:t(c),chartY:t(b)})},getMouseCoordinates:function(a){var b={xAxis:[],yAxis:[]},c=this.chart;n(c.axes,function(d){var e=d.isXAxis;b[e?"xAxis":"yAxis"].push({axis:d,value:d.translate((c.inverted?!e:e)?a.chartX-c.plotLeft:c.plotHeight-a.chartY+c.plotTop,!0)})});return b},onmousemove:function(a){var b= -this.chart,c=b.series,d,e,f=b.hoverPoint,g=b.hoverSeries,h,i,j=b.chartWidth,k=b.inverted?b.plotHeight+b.plotTop-a.chartY:a.chartX-b.plotLeft;if(b.tooltip&&this.options.tooltip.shared&&(!g||!g.noSharedTooltip)){e=[];h=c.length;for(i=0;ij&&e.splice(h,1);if(e.length&&e[0].plotX!==this.hoverX)b.tooltip.refresh(e, -a),this.hoverX=e[0].plotX}if(g&&g.tracker&&(d=g.tooltipPoints[k])&&d!==f)d.onMouseOver()},resetTracker:function(a){var b=this.chart,c=b.hoverSeries,d=b.hoverPoint,e=b.hoverPoints||d,b=b.tooltip;(a=a&&b&&e)&&na(e)[0].plotX===A&&(a=!1);if(a)b.refresh(e);else{if(d)d.onMouseOut();if(c)c.onMouseOut();b&&(b.hide(),b.hideCrosshairs());this.hoverX=null}},setDOMEvents:function(){function a(){if(b.selectionMarker){var f={xAxis:[],yAxis:[]},g=b.selectionMarker.getBBox(),h=g.x-c.plotLeft,l=g.y-c.plotTop,m;e&& -(n(c.axes,function(a){if(a.options.zoomEnabled!==!1){var b=a.isXAxis,d=c.inverted?!b:b,e=a.translate(d?h:c.plotHeight-l-g.height,!0,0,0,1),d=a.translate(d?h+g.width:c.plotHeight-l,!0,0,0,1);!isNaN(e)&&!isNaN(d)&&(f[b?"xAxis":"yAxis"].push({axis:a,min:O(e,d),max:x(e,d)}),m=!0)}}),m&&E(c,"selection",f,function(a){c.zoom(a)}));b.selectionMarker=b.selectionMarker.destroy()}if(c)F(d,{cursor:"auto"}),c.cancelClick=e,c.mouseIsDown=e=!1;P(B,ea?"touchend":"mouseup",a)}var b=this,c=b.chart,d=c.container,e, -f=b.zoomX&&!c.inverted||b.zoomY&&c.inverted,g=b.zoomY&&!c.inverted||b.zoomX&&c.inverted;b.hideTooltipOnMouseMove=function(a){Kb(a);b.chartPosition&&c.hoverSeries&&c.hoverSeries.isCartesian&&!c.isInsidePlot(a.pageX-b.chartPosition.left-c.plotLeft,a.pageY-b.chartPosition.top-c.plotTop)&&b.resetTracker()};b.hideTooltipOnMouseLeave=function(){b.resetTracker();b.chartPosition=null};d.onmousedown=function(d){d=b.normalizeMouseEvent(d);!ea&&d.preventDefault&&d.preventDefault();c.mouseIsDown=!0;c.cancelClick= -!1;c.mouseDownX=b.mouseDownX=d.chartX;b.mouseDownY=d.chartY;I(B,ea?"touchend":"mouseup",a)};var h=function(a){if(!a||!(a.touches&&a.touches.length>1)){a=b.normalizeMouseEvent(a);if(!ea)a.returnValue=!1;var d=a.chartX,h=a.chartY,l=!c.isInsidePlot(d-c.plotLeft,h-c.plotTop);ea&&a.type==="touchstart"&&(z(a.target,"isTracker")?c.runTrackerClick||a.preventDefault():!c.runChartClick&&!l&&a.preventDefault());if(l)dc.plotLeft+c.plotWidth&&(d=c.plotLeft+c.plotWidth),hc.plotTop+c.plotHeight&&(h=c.plotTop+c.plotHeight);if(c.mouseIsDown&&a.type!=="touchstart"){if(e=Math.sqrt(Math.pow(b.mouseDownX-d,2)+Math.pow(b.mouseDownY-h,2)),e>10){a=c.isInsidePlot(b.mouseDownX-c.plotLeft,b.mouseDownY-c.plotTop);if(c.hasCartesianSeries&&(b.zoomX||b.zoomY)&&a&&!b.selectionMarker)b.selectionMarker=c.renderer.rect(c.plotLeft,c.plotTop,f?1:c.plotWidth,g?1:c.plotHeight,0).attr({fill:b.options.chart.selectionMarkerFill||"rgba(69,114,167,0.25)",zIndex:7}).add();if(b.selectionMarker&& -f){var m=d-b.mouseDownX;b.selectionMarker.attr({width:M(m),x:(m>0?0:m)+b.mouseDownX})}b.selectionMarker&&g&&(h-=b.mouseDownY,b.selectionMarker.attr({height:M(h),y:(h>0?0:h)+b.mouseDownY}));a&&!b.selectionMarker&&b.options.chart.panning&&c.pan(d)}}else if(!l)b.onmousemove(a);return l||!c.hasCartesianSeries}};d.onmousemove=h;I(d,"mouseleave",b.hideTooltipOnMouseLeave);I(B,"mousemove",b.hideTooltipOnMouseMove);d.ontouchstart=function(a){if(b.zoomX||b.zoomY)d.onmousedown(a);h(a)};d.ontouchmove=h;d.ontouchend= -function(){e&&b.resetTracker()};d.onclick=function(a){var d=c.hoverPoint,e,f,a=b.normalizeMouseEvent(a);a.cancelBubble=!0;if(!c.cancelClick)d&&(z(a.target,"isTracker")||z(a.target.parentNode,"isTracker"))?(e=d.plotX,f=d.plotY,u(d,{pageX:b.chartPosition.left+c.plotLeft+(c.inverted?c.plotWidth-f:e),pageY:b.chartPosition.top+c.plotTop+(c.inverted?c.plotHeight-e:f)}),E(d.series,"click",u(a,{point:d})),d.firePointEvent("click",a)):(u(a,b.getMouseCoordinates(a)),c.isInsidePlot(a.chartX-c.plotLeft,a.chartY- -c.plotTop)&&E(c,"click",a))}},destroy:function(){var a=this.chart,b=a.container;if(a.trackerGroup)a.trackerGroup=a.trackerGroup.destroy();P(b,"mouseleave",this.hideTooltipOnMouseLeave);P(B,"mousemove",this.hideTooltipOnMouseMove);b.onclick=b.onmousedown=b.onmousemove=b.ontouchstart=b.ontouchend=b.ontouchmove=null;clearInterval(this.tooltipInterval)},init:function(a,b){if(!a.trackerGroup)a.trackerGroup=a.renderer.g("tracker").attr({zIndex:9}).add();if(b.enabled)a.tooltip=new pb(a,b),this.tooltipInterval= -setInterval(function(){a.tooltip.tick()},32);this.setDOMEvents()}};qb.prototype={init:function(a){var b=this,c=b.options=a.options.legend;if(c.enabled){var d=c.itemStyle,e=o(c.padding,8),f=c.itemMarginTop||0;b.baseline=w(d.fontSize)+3+f;b.itemStyle=d;b.itemHiddenStyle=C(d,c.itemHiddenStyle);b.itemMarginTop=f;b.padding=e;b.initialItemX=e;b.initialItemY=e-5;b.maxItemWidth=0;b.chart=a;b.itemHeight=0;b.lastLineHeight=0;b.render();I(b.chart,"endResize",function(){b.positionCheckboxes()})}},colorizeItem:function(a, -b){var c=this.options,d=a.legendItem,e=a.legendLine,f=a.legendSymbol,g=this.itemHiddenStyle.color,c=b?c.itemStyle.color:g,g=b?a.color:g;d&&d.css({fill:c});e&&e.attr({stroke:g});f&&f.attr({stroke:g,fill:g})},positionItem:function(a){var b=this.options,c=b.symbolPadding,b=!b.rtl,d=a._legendItemPos,e=d[0],d=d[1],f=a.checkbox;a.legendGroup&&a.legendGroup.translate(b?e:this.legendWidth-e-2*c-4,d);if(f)f.x=e,f.y=d},destroyItem:function(a){var b=a.checkbox;n(["legendItem","legendLine","legendSymbol","legendGroup"], -function(b){a[b]&&a[b].destroy()});b&&Qa(a.checkbox)},destroy:function(){var a=this.group,b=this.box;if(b)this.box=b.destroy();if(a)this.group=a.destroy()},positionCheckboxes:function(){var a=this;n(a.allItems,function(b){var c=b.checkbox,d=a.group.alignAttr;c&&F(c,{left:d.translateX+b.legendItemWidth+c.x-20+"px",top:d.translateY+c.y+3+"px"})})},renderItem:function(a){var q;var b=this,c=b.chart,d=c.renderer,e=b.options,f=e.layout==="horizontal",g=e.symbolWidth,h=e.symbolPadding,i=b.itemStyle,j=b.itemHiddenStyle, -k=b.padding,l=!e.rtl,m=e.width,p=e.itemMarginBottom||0,n=b.itemMarginTop,o=b.initialItemX,r=a.legendItem,s=a.series||a,t=s.options,u=t.showCheckbox;if(!r&&(a.legendGroup=d.g("legend-item").attr({zIndex:1}).add(b.scrollGroup),s.drawLegendSymbol(b,a),a.legendItem=r=d.text(e.labelFormatter.call(a),l?g+h:-h,b.baseline,e.useHTML).css(C(a.visible?i:j)).attr({align:l?"left":"right",zIndex:2}).add(a.legendGroup),a.legendGroup.on("mouseover",function(){a.setState("hover");r.css(b.options.itemHoverStyle)}).on("mouseout", -function(){r.css(a.visible?i:j);a.setState()}).on("click",function(b){var c=function(){a.setVisible()},b={browserEvent:b};a.firePointEvent?a.firePointEvent("legendItemClick",b,c):E(a,"legendItemClick",b,c)}),b.colorizeItem(a,a.visible),t&&u))a.checkbox=S("input",{type:"checkbox",checked:a.selected,defaultChecked:a.selected},e.itemCheckboxStyle,c.container),I(a.checkbox,"click",function(b){E(a,"checkboxClick",{checked:b.target.checked},function(){a.select()})});d=r.getBBox();q=a.legendItemWidth=e.itemWidth|| -g+h+d.width+k+(u?20:0),e=q;b.itemHeight=g=d.height;if(f&&b.itemX-o+e>(m||c.chartWidth-2*k-o))b.itemX=o,b.itemY+=n+b.lastLineHeight+p,b.lastLineHeight=0;b.maxItemWidth=x(b.maxItemWidth,e);b.lastItemY=n+b.itemY+p;b.lastLineHeight=x(g,b.lastLineHeight);a._legendItemPos=[b.itemX,b.itemY];f?b.itemX+=e:(b.itemY+=n+g+p,b.lastLineHeight=g);b.offsetWidth=m||x(f?b.itemX-o:e,b.offsetWidth)},render:function(){var a=this,b=a.chart,c=b.renderer,d=a.group,e,f,g,h,i=a.box,j=a.options,k=a.padding,l=j.borderWidth, -m=j.backgroundColor;a.itemX=a.initialItemX;a.itemY=a.initialItemY;a.offsetWidth=0;a.lastItemY=0;if(!d)a.group=d=c.g("legend").attr({zIndex:7}).add(),a.contentGroup=c.g().attr({zIndex:1}).add(d),a.scrollGroup=c.g().add(a.contentGroup),a.clipRect=c.clipRect(0,0,9999,b.chartHeight),a.contentGroup.clip(a.clipRect);e=[];n(b.series,function(a){var b=a.options;b.showInLegend&&(e=e.concat(a.legendItems||(b.legendType==="point"?a.data:a)))});Qb(e,function(a,b){return(a.options.legendIndex||0)-(b.options.legendIndex|| -0)});j.reversed&&e.reverse();a.allItems=e;a.display=f=!!e.length;n(e,function(b){a.renderItem(b)});g=j.width||a.offsetWidth;h=a.lastItemY+a.lastLineHeight;h=a.handleOverflow(h);if(l||m){g+=k;h+=k;if(i){if(g>0&&h>0)i[i.isNew?"attr":"animate"](i.crisp(null,null,null,g,h)),i.isNew=!1}else a.box=i=c.rect(0,0,g,h,j.borderRadius,l||0).attr({stroke:j.borderColor,"stroke-width":l||0,fill:m||U}).add(d).shadow(j.shadow),i.isNew=!0;i[f?"show":"hide"]()}a.legendWidth=g;a.legendHeight=h;n(e,function(b){a.positionItem(b)}); -f&&d.align(u({width:g,height:h},j),!0,b.spacingBox);b.isResizing||this.positionCheckboxes()},handleOverflow:function(a){var b=this,c=this.chart,d=c.renderer,e=this.options,f=e.y,f=c.spacingBox.height+(e.verticalAlign==="top"?-f:f)-this.padding,g=e.maxHeight,h=this.clipRect,i=e.navigation,j=o(i.animation,!0),k=i.arrowSize||12,l=this.nav;e.layout==="horizontal"&&(f/=2);g&&(f=O(f,g));if(a>f){this.clipHeight=c=f-20;this.pageCount=wa(a/c);this.currentPage=o(this.currentPage,1);this.fullHeight=a;h.attr({height:c}); -if(!l)this.nav=l=d.g().attr({zIndex:1}).add(this.group),this.up=d.symbol("triangle",0,0,k,k).on("click",function(){b.scroll(-1,j)}).add(l),this.pager=d.text("",15,10).css(i.style).add(l),this.down=d.symbol("triangle-down",0,0,k,k).on("click",function(){b.scroll(1,j)}).add(l);b.scroll(0);a=f}else l&&(h.attr({height:c.chartHeight}),l.hide(),this.scrollGroup.attr({translateY:1}));return a},scroll:function(a,b){var c=this.pageCount,d=this.currentPage+a,e=this.clipHeight,f=this.options.navigation,g=f.activeColor, -f=f.inactiveColor,h=this.pager,i=this.padding;d>c&&(d=c);if(d>0)b!==A&&ua(b,this.chart),this.nav.attr({translateX:i,translateY:e+7,visibility:"visible"}),this.up.attr({fill:d===1?f:g}).css({cursor:d===1?"default":"pointer"}),h.attr({text:d+"/"+this.pageCount}),this.down.attr({x:18+this.pager.getBBox().width,fill:d===c?f:g}).css({cursor:d===c?"default":"pointer"}),this.scrollGroup.animate({translateY:-O(e*(d-1),this.fullHeight-e+i)+1}),h.attr({text:d+"/"+c}),this.currentPage=d}};rb.prototype={initSeries:function(a){var b= -this.options.chart,b=new Y[a.type||b.type||b.defaultSeriesType];b.init(this,a);return b},addSeries:function(a,b,c){var d=this;a&&(ua(c,d),b=o(b,!0),E(d,"addSeries",{options:a},function(){d.initSeries(a);d.isDirtyLegend=!0;b&&d.redraw()}))},isInsidePlot:function(a,b){return a>=0&&a<=this.plotWidth&&b>=0&&b<=this.plotHeight},adjustTickAmounts:function(){this.options.chart.alignTicks!==!1&&n(this.axes,function(a){a.adjustTickAmount()});this.maxTicks=null},redraw:function(a){var b=this.axes,c=this.series, -d=this.tracker,e=this.legend,f=this.isDirtyLegend,g,h=this.isDirtyBox,i=c.length,j=i,k=this.clipRect,l=this.renderer,m=l.isHidden();ua(a,this);for(m&&this.cloneRenderTo();j--;)if(a=c[j],a.isDirty&&a.options.stacking){g=!0;break}if(g)for(j=i;j--;)if(a=c[j],a.options.stacking)a.isDirty=!0;n(c,function(a){a.isDirty&&a.options.legendType==="point"&&(f=!0)});if(f&&e.options.enabled)e.render(),this.isDirtyLegend=!1;if(this.hasCartesianSeries){if(!this.isResizing)this.maxTicks=null,n(b,function(a){a.setScale()}); -this.adjustTickAmounts();this.getMargins();n(b,function(a){if(a.isDirtyExtremes)a.isDirtyExtremes=!1,E(a,"afterSetExtremes",a.getExtremes());if(a.isDirty||h||g)a.redraw(),h=!0})}h&&(this.drawChartBox(),k&&(Fa(k),k.animate({width:this.plotSizeX,height:this.plotSizeY+1})));n(c,function(a){a.isDirty&&a.visible&&(!a.isCartesian||a.xAxis)&&a.redraw()});d&&d.resetTracker&&d.resetTracker(!0);l.draw();E(this,"redraw");m&&this.cloneRenderTo(!0)},showLoading:function(a){var b=this.options,c=this.loadingDiv, -d=b.loading;if(!c)this.loadingDiv=c=S(ia,{className:"highcharts-loading"},u(d.style,{left:this.plotLeft+"px",top:this.plotTop+"px",width:this.plotWidth+"px",height:this.plotHeight+"px",zIndex:10,display:U}),this.container),this.loadingSpan=S("span",null,d.labelStyle,c);this.loadingSpan.innerHTML=a||b.lang.loading;if(!this.loadingShown)F(c,{opacity:0,display:""}),eb(c,{opacity:d.style.opacity},{duration:d.showDuration||0}),this.loadingShown=!0},hideLoading:function(){var a=this.options,b=this.loadingDiv; -b&&eb(b,{opacity:0},{duration:a.loading.hideDuration||100,complete:function(){F(b,{display:U})}});this.loadingShown=!1},get:function(a){var b=this.axes,c=this.series,d,e;for(d=0;dO(e.dataMin,e.min)&&c19?this.containerHeight:400)},cloneRenderTo:function(a){var b=this.renderToClone,c=this.container;a?b&&(this.renderTo.appendChild(c),Qa(b),delete this.renderToClone):(c&&this.renderTo.removeChild(c),this.renderToClone=b=this.renderTo.cloneNode(0),F(b,{position:"absolute",top:"-9999px",display:"block"}), -B.body.appendChild(b),c&&b.appendChild(c))},getContainer:function(){var a,b=this.options.chart,c,d,e;this.renderTo=a=b.renderTo;e="highcharts-"+sb++;if(ra(a))this.renderTo=a=B.getElementById(a);a||mb(13,!0);a.innerHTML="";a.offsetWidth||this.cloneRenderTo();this.getChartSize();c=this.chartWidth;d=this.chartHeight;this.container=a=S(ia,{className:"highcharts-container"+(b.className?" "+b.className:""),id:e},u({position:"relative",overflow:"hidden",width:c+"px",height:d+"px",textAlign:"left",lineHeight:"normal"}, -b.style),this.renderToClone||a);this.renderer=b.forExport?new qa(a,c,d,!0):new Ta(a,c,d);ga&&this.renderer.create(this,a,c,d)},getMargins:function(){var a=this.options.chart,b=a.spacingTop,c=a.spacingRight,d=a.spacingBottom,a=a.spacingLeft,e,f=this.legend,g=this.optionsMarginTop,h=this.optionsMarginLeft,i=this.optionsMarginRight,j=this.optionsMarginBottom,k=this.chartTitleOptions,l=this.chartSubtitleOptions,m=this.options.legend,p=o(m.margin,10),q=m.x,t=m.y,r=m.align,y=m.verticalAlign;this.resetMargins(); -e=this.axisOffset;if((this.title||this.subtitle)&&!s(this.optionsMarginTop))if(l=x(this.title&&!k.floating&&!k.verticalAlign&&k.y||0,this.subtitle&&!l.floating&&!l.verticalAlign&&l.y||0))this.plotTop=x(this.plotTop,l+o(k.margin,15)+b);if(f.display&&!m.floating)if(r==="right"){if(!s(i))this.marginRight=x(this.marginRight,f.legendWidth-q+p+c)}else if(r==="left"){if(!s(h))this.plotLeft=x(this.plotLeft,f.legendWidth+q+p+a)}else if(y==="top"){if(!s(g))this.plotTop=x(this.plotTop,f.legendHeight+t+p+b)}else if(y=== -"bottom"&&!s(j))this.marginBottom=x(this.marginBottom,f.legendHeight-t+p+d);this.extraBottomMargin&&(this.marginBottom+=this.extraBottomMargin);this.extraTopMargin&&(this.plotTop+=this.extraTopMargin);this.hasCartesianSeries&&n(this.axes,function(a){a.getOffset()});s(h)||(this.plotLeft+=e[3]);s(g)||(this.plotTop+=e[0]);s(j)||(this.marginBottom+=e[2]);s(i)||(this.marginRight+=e[1]);this.setChartSize()},initReflow:function(){function a(a){var g=c.width||Va(d,"width"),h=c.height||Va(d,"height"),a=a? -a.target:N;if(g&&h&&(a===N||a===B)){if(g!==b.containerWidth||h!==b.containerHeight)clearTimeout(e),e=setTimeout(function(){b.resize(g,h,!1)},100);b.containerWidth=g;b.containerHeight=h}}var b=this,c=b.options.chart,d=b.renderTo,e;I(N,"resize",a);I(b,"destroy",function(){P(N,"resize",a)})},fireEndResize:function(){var a=this;a&&E(a,"endResize",null,function(){a.isResizing-=1})},resize:function(a,b,c){var d,e,f=this.title,g=this.subtitle;this.isResizing+=1;ua(c,this);this.oldChartHeight=this.chartHeight; -this.oldChartWidth=this.chartWidth;if(s(a))this.chartWidth=d=t(a);if(s(b))this.chartHeight=e=t(b);F(this.container,{width:d+"px",height:e+"px"});this.renderer.setSize(d,e,c);this.plotWidth=d-this.plotLeft-this.marginRight;this.plotHeight=e-this.plotTop-this.marginBottom;this.maxTicks=null;n(this.axes,function(a){a.isDirty=!0;a.setScale()});n(this.series,function(a){a.isDirty=!0});this.isDirtyBox=this.isDirtyLegend=!0;this.getMargins();a=this.spacingBox;f&&f.align(null,null,a);g&&g.align(null,null, -a);this.redraw(c);this.oldChartHeight=null;E(this,"resize");Ra===!1?this.fireEndResize():setTimeout(this.fireEndResize,Ra&&Ra.duration||500)},setChartSize:function(){var a=this.inverted,b=this.chartWidth,c=this.chartHeight,d=this.options.chart,e=d.spacingTop,f=d.spacingRight,g=d.spacingBottom,d=d.spacingLeft;this.plotLeft=t(this.plotLeft);this.plotTop=t(this.plotTop);this.plotWidth=t(b-this.plotLeft-this.marginRight);this.plotHeight=t(c-this.plotTop-this.marginBottom);this.plotSizeX=a?this.plotHeight: -this.plotWidth;this.plotSizeY=a?this.plotWidth:this.plotHeight;this.spacingBox={x:d,y:e,width:b-d-f,height:c-e-g};n(this.axes,function(a){a.setAxisSize();a.setAxisTranslation()})},resetMargins:function(){var a=this.options.chart,b=a.spacingRight,c=a.spacingBottom,d=a.spacingLeft;this.plotTop=o(this.optionsMarginTop,a.spacingTop);this.marginRight=o(this.optionsMarginRight,b);this.marginBottom=o(this.optionsMarginBottom,c);this.plotLeft=o(this.optionsMarginLeft,d);this.axisOffset=[0,0,0,0]},drawChartBox:function(){var a= -this.options.chart,b=this.renderer,c=this.chartWidth,d=this.chartHeight,e=this.chartBackground,f=this.plotBackground,g=this.plotBorder,h=this.plotBGImage,i=a.borderWidth||0,j=a.backgroundColor,k=a.plotBackgroundColor,l=a.plotBackgroundImage,m,p={x:this.plotLeft,y:this.plotTop,width:this.plotWidth,height:this.plotHeight};m=i+(a.shadow?8:0);if(i||j)if(e)e.animate(e.crisp(null,null,null,c-m,d-m));else{e={fill:j||U};if(i)e.stroke=a.borderColor,e["stroke-width"]=i;this.chartBackground=b.rect(m/2,m/2,c- -m,d-m,a.borderRadius,i).attr(e).add().shadow(a.shadow)}if(k)f?f.animate(p):this.plotBackground=b.rect(this.plotLeft,this.plotTop,this.plotWidth,this.plotHeight,0).attr({fill:k}).add().shadow(a.plotShadow);if(l)h?h.animate(p):this.plotBGImage=b.image(l,this.plotLeft,this.plotTop,this.plotWidth,this.plotHeight).add();if(a.plotBorderWidth)g?g.animate(g.crisp(null,this.plotLeft,this.plotTop,this.plotWidth,this.plotHeight)):this.plotBorder=b.rect(this.plotLeft,this.plotTop,this.plotWidth,this.plotHeight, -0,a.plotBorderWidth).attr({stroke:a.plotBorderColor,"stroke-width":a.plotBorderWidth,zIndex:4}).add();this.isDirtyBox=!1},propFromSeries:function(){var a=this,b=a.options.chart,c,d=a.options.series,e,f;n(["inverted","angular","polar"],function(g){c=Y[b.type||b.defaultSeriesType];f=a[g]||b[g]||c&&c.prototype[g];for(e=d&&d.length;!f&&e--;)(c=Y[d[e].type])&&c.prototype[g]&&(f=!0);a[g]=f})},render:function(){var a=this,b=a.axes,c=a.renderer,d=a.options,e=d.labels,d=d.credits,f;a.setTitle();a.legend=new qb(a); -n(b,function(a){a.setScale()});a.getMargins();a.maxTicks=null;n(b,function(a){a.setTickPositions(!0);a.setMaxTicks()});a.adjustTickAmounts();a.getMargins();a.drawChartBox();a.hasCartesianSeries&&n(b,function(a){a.render()});if(!a.seriesGroup)a.seriesGroup=c.g("series-group").attr({zIndex:3}).add();n(a.series,function(a){a.translate();a.setTooltipPoints();a.render()});e.items&&n(e.items,function(){var b=u(e.style,this.style),d=w(b.left)+a.plotLeft,f=w(b.top)+a.plotTop+12;delete b.left;delete b.top; -c.text(this.html,d,f).attr({zIndex:2}).css(b).add()});if(d.enabled&&!a.credits)f=d.href,a.credits=c.text(d.text,0,0).on("click",function(){if(f)location.href=f}).attr({align:d.position.align,zIndex:8}).css(d.style).add().align(d.position);a.hasRendered=!0},destroy:function(){var a=this,b=a.axes,c=a.series,d=a.container,e,f=d&&d.parentNode;if(a!==null){E(a,"destroy");P(a);for(e=b.length;e--;)b[e]=b[e].destroy();for(e=c.length;e--;)c[e]=c[e].destroy();n("title,subtitle,chartBackground,plotBackground,plotBGImage,plotBorder,seriesGroup,clipRect,credits,tracker,scroller,rangeSelector,legend,resetZoomButton,tooltip,renderer".split(","), -function(b){var c=a[b];c&&(a[b]=c.destroy())});if(d)d.innerHTML="",P(d),f&&Qa(d),d=null;for(e in a)delete a[e];a=a.options=null}},firstRender:function(){var a=this,b=a.options,c=a.callback;if(!Da&&N==N.top&&B.readyState!=="complete"||ga&&!N.canvg)ga?Nb.push(function(){a.firstRender()},b.global.canvasToolsURL):B.attachEvent("onreadystatechange",function(){B.detachEvent("onreadystatechange",a.firstRender);B.readyState==="complete"&&a.firstRender()});else{a.getContainer();E(a,"init");if(Highcharts.RangeSelector&& -b.rangeSelector.enabled)a.rangeSelector=new Highcharts.RangeSelector(a);a.resetMargins();a.setChartSize();a.propFromSeries();a.getAxes();n(b.series||[],function(b){a.initSeries(b)});if(Highcharts.Scroller&&(b.navigator.enabled||b.scrollbar.enabled))a.scroller=new Highcharts.Scroller(a);a.tracker=new Db(a,b);a.render();a.renderer.draw();c&&c.apply(a,[a]);n(a.callbacks,function(b){b.apply(a,[a])});a.cloneRenderTo(!0);E(a,"load")}},init:function(a){var b=this.options.chart,c;b.reflow!==!1&&I(this,"load", -this.initReflow);if(a)for(c in a)I(this,c,a[c]);this.xAxis=[];this.yAxis=[];this.animation=ga?!1:o(b.animation,!0);this.setSize=this.resize;this.pointCount=0;this.counters=new Ab;this.firstRender()}};rb.prototype.callbacks=[];var Wa=function(){};Wa.prototype={init:function(a,b,c){var d=a.chart.counters;this.series=a;this.applyOptions(b,c);this.pointAttr={};if(a.options.colorByPoint){b=a.chart.options.colors;if(!this.options)this.options={};this.color=this.options.color=this.color||b[d.color++];d.wrapColor(b.length)}a.chart.pointCount++; -return this},applyOptions:function(a,b){var c=this.series,d=typeof a;this.config=a;if(d==="number"||a===null)this.y=a;else if(typeof a[0]==="number")this.x=a[0],this.y=a[1];else if(d==="object"&&typeof a.length!=="number"){if(u(this,a),this.options=a,a.dataLabels)c._hasPointLabels=!0}else if(typeof a[0]==="string")this.name=a[0],this.y=a[1];if(this.x===A)this.x=b===A?c.autoIncrement():b},destroy:function(){var a=this.series.chart,b=a.hoverPoints,c;a.pointCount--;if(b&&(this.setState(),za(b,this), -!b.length))a.hoverPoints=null;if(this===a.hoverPoint)this.onMouseOut();if(this.graphic||this.dataLabel)P(this),this.destroyElements();this.legendItem&&a.legend.destroyItem(this);for(c in this)this[c]=null},destroyElements:function(){for(var a="graphic,tracker,dataLabel,group,connector,shadowGroup".split(","),b,c=6;c--;)b=a[c],this[b]&&(this[b]=this[b].destroy())},getLabelConfig:function(){return{x:this.category,y:this.y,key:this.name||this.category,series:this.series,point:this,percentage:this.percentage, -total:this.total||this.stackTotal}},select:function(a,b){var c=this,d=c.series.chart,a=o(a,!c.selected);c.firePointEvent(a?"select":"unselect",{accumulate:b},function(){c.selected=a;c.setState(a&&"select");b||n(d.getSelectedPoints(),function(a){if(a.selected&&a!==c)a.selected=!1,a.setState(""),a.firePointEvent("unselect")})})},onMouseOver:function(){var a=this.series,b=a.chart,c=b.tooltip,d=b.hoverPoint;if(d&&d!==this)d.onMouseOut();this.firePointEvent("mouseOver");c&&(!c.shared||a.noSharedTooltip)&& -c.refresh(this);this.setState("hover");b.hoverPoint=this},onMouseOut:function(){this.firePointEvent("mouseOut");this.setState();this.series.chart.hoverPoint=null},tooltipFormatter:function(a){var b=this.series,c=b.tooltipOptions,d=a.match(/\{(series|point)\.[a-zA-Z]+\}/g),e=/[{\.}]/,f,g,h,i,j={y:0,open:0,high:0,low:0,close:0,percentage:1,total:1};c.valuePrefix=c.valuePrefix||c.yPrefix;c.valueDecimals=c.valueDecimals||c.yDecimals;c.valueSuffix=c.valueSuffix||c.ySuffix;for(i in d)g=d[i],ra(g)&&g!== -a&&(h=(" "+g).split(e),f={point:this,series:b}[h[1]],h=h[2],f===this&&j.hasOwnProperty(h)?(f=j[h]?h:"value",f=(c[f+"Prefix"]||"")+Xa(this[h],o(c[f+"Decimals"],-1))+(c[f+"Suffix"]||"")):f=f[h],a=a.replace(g,f));return a},update:function(a,b,c){var d=this,e=d.series,f=d.graphic,g,h=e.data,i=h.length,j=e.chart,b=o(b,!0);d.firePointEvent("update",{options:a},function(){d.applyOptions(a);aa(a)&&(e.getAttribs(),f&&f.attr(d.pointAttr[e.state]));for(g=0;ga+1&&b.push(d.slice(a+1,g)),a=g):g===e-1&&b.push(d.slice(a+1,g+1))});this.segments=b},setOptions:function(a){var b=this.chart.options,c=b.plotOptions,d=a.data;a.data=null;c=C(c[this.type],c.series,a);c.data=a.data=d;this.tooltipOptions=C(b.tooltip,c.tooltip);return c},getColor:function(){var a= -this.options,b=this.chart.options.colors,c=this.chart.counters;this.color=a.color||!a.colorByPoint&&b[c.color++]||"gray";c.wrapColor(b.length)},getSymbol:function(){var a=this.options.marker,b=this.chart,c=b.options.symbols,b=b.counters;this.symbol=a.symbol||c[b.symbol++];if(/^url/.test(this.symbol))a.radius=0;b.wrapSymbol(c.length)},drawLegendSymbol:function(a){var b=this.options,c=b.marker,d=a.options.symbolWidth,e=this.chart.renderer,f=this.legendGroup,a=a.baseline,g;if(b.lineWidth){g={"stroke-width":b.lineWidth}; -if(b.dashStyle)g.dashstyle=b.dashStyle;this.legendLine=e.path(["M",0,a-4,"L",d,a-4]).attr(g).add(f)}if(c&&c.enabled)b=c.radius,this.legendSymbol=e.symbol(this.symbol,d/2-b,a-4-b,2*b,2*b).attr(this.pointAttr[""]).add(f)},addPoint:function(a,b,c,d){var e=this.data,f=this.graph,g=this.area,h=this.chart,i=this.xData,j=this.yData,k=f&&f.shift||0,l=this.options.data;ua(d,h);if(f&&c)f.shift=k+1;if(g){if(c)g.shift=k+1;g.isArea=!0}b=o(b,!0);d={series:this};this.pointClass.prototype.applyOptions.apply(d,[a]); -i.push(d.x);j.push(this.valueCount===4?[d.open,d.high,d.low,d.close]:d.y);l.push(a);c&&(e[0]&&e[0].remove?e[0].remove(!1):(e.shift(),i.shift(),j.shift(),l.shift()));this.getAttribs();this.isDirtyData=this.isDirty=!0;b&&h.redraw()},setData:function(a,b){var c=this.points,d=this.options,e=this.initialColor,f=this.chart,g=null,h=this.xAxis,i=this.pointClass.prototype;this.xIncrement=null;this.pointRange=h&&h.categories&&1||d.pointRange;if(s(e))f.counters.color=e;var j=[],k=[],l=a?a.length:[],m=this.valueCount; -if(l>(d.turboThreshold||1E3)){for(e=0;g===null&&ek||this.forceCrop))if(a=i.getExtremes(),i=a.min,k=a.max,b[d-1]k)b=[],c=[];else if(b[0]k){for(a=0;a=i){e=x(0,a-1);break}for(;ak){f=a+1;break}b=b.slice(e,f);c=c.slice(e,f);g=!0}for(a=b.length-1;a>0;a--)if(d=b[a]-b[a-1],d>0&&(h===A||d=0&&d<=e;)h[d++]=f}this.tooltipPoints=h}},tooltipHeaderFormatter:function(a){var b=this.tooltipOptions,c=b.xDateFormat,d=this.xAxis,e=d&&d.options.type==="datetime",f;if(e&&!c)for(f in D)if(D[f]>=d.closestPointRange){c=b.dateTimeLabelFormats[f];break}return b.headerFormat.replace("{point.key}",e?db(c,a):a).replace("{series.name}", -this.name).replace("{series.color}",this.color)},onMouseOver:function(){var a=this.chart,b=a.hoverSeries;if(ea||!a.mouseIsDown){if(b&&b!==this)b.onMouseOut();this.options.events.mouseOver&&E(this,"mouseOver");this.setState("hover");a.hoverSeries=this}},onMouseOut:function(){var a=this.options,b=this.chart,c=b.tooltip,d=b.hoverPoint;if(d)d.onMouseOut();this&&a.events.mouseOut&&E(this,"mouseOut");c&&!a.stickyTracking&&!c.shared&&c.hide();this.setState();b.hoverSeries=null},animate:function(a){var b= -this.chart,c=this.clipRect,d=this.options.animation;d&&!aa(d)&&(d={});if(a){if(!c.isAnimating)c.attr("width",0),c.isAnimating=!0}else c.animate({width:b.plotSizeX},d),this.animate=null},drawPoints:function(){var a,b=this.points,c=this.chart,d,e,f,g,h,i,j,k;if(this.options.marker.enabled)for(f=b.length;f--;)if(g=b[f],d=g.plotX,e=g.plotY,k=g.graphic,e!==A&&!isNaN(e))if(a=g.pointAttr[g.selected?"select":""],h=a.r,i=o(g.marker&&g.marker.symbol,this.symbol),j=i.indexOf("url")===0,k)k.animate(u({x:d-h, -y:e-h},k.symbolName?{width:2*h,height:2*h}:{}));else if(h>0||j)g.graphic=c.renderer.symbol(i,d-h,e-h,2*h,2*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]=o(a[g],b[f],c[f],d[f]);return h},getAttribs:function(){var a=this,b=Z[a.type].marker?a.options.marker:a.options,c=b.states,d=c.hover,e,f=a.color,g={stroke:f,fill:f},h=a.points||[],i=[],j,k=a.pointAttrToOptions,l;a.options.marker?(d.radius= -d.radius||b.radius+2,d.lineWidth=d.lineWidth||b.lineWidth+1):d.color=d.color||pa(d.color||f).brighten(d.brightness).get();i[""]=a.convertAttribs(b,g);n(["hover","select"],function(b){i[b]=a.convertAttribs(c[b],i[""])});a.pointAttr=i;for(f=h.length;f--;){g=h[f];if((b=g.options&&g.options.marker||g.options)&&b.enabled===!1)b.radius=0;e=!1;if(g.options)for(l in k)s(b[k[l]])&&(e=!0);if(e){j=[];c=b.states||{};e=c.hover=c.hover||{};if(!a.options.marker)e.color=pa(e.color||g.options.color).brighten(e.brightness|| -d.brightness).get();j[""]=a.convertAttribs(b,i[""]);j.hover=a.convertAttribs(c.hover,i.hover,j[""]);j.select=a.convertAttribs(c.select,i.select,j[""])}else j=i;g.pointAttr=j}},destroy:function(){var a=this,b=a.chart,c=a.clipRect,d=/AppleWebKit\/533/.test(ya),e,f,g=a.data||[],h,i,j;E(a,"destroy");P(a);n(["xAxis","yAxis"],function(b){if(j=a[b])za(j.series,a),j.isDirty=!0});a.legendItem&&a.chart.legend.destroyItem(a);for(f=g.length;f--;)(h=g[f])&&h.destroy&&h.destroy();a.points=null;if(c&&c!==b.clipRect)a.clipRect= -c.destroy();n("area,graph,dataLabelsGroup,group,tracker,trackerGroup".split(","),function(b){a[b]&&(e=d&&b==="group"?"hide":"destroy",a[b][e]())});if(b.hoverSeries===a)b.hoverSeries=null;za(b.series,a);for(i in a)delete a[i]},drawDataLabels:function(){var a=this,b=a.options,c=b.dataLabels;if(c.enabled||a._hasPointLabels){var d,e,f=a.points,g,h,i,j=a.dataLabelsGroup,k=a.chart,l=a.xAxis,l=l?l.left:k.plotLeft,m=a.yAxis,m=m?m.top:k.plotTop,p=k.renderer,q=k.inverted,u=a.type,r=b.stacking,y=u==="column"|| -u==="bar",x=c.verticalAlign===null,w=c.y===null,v=p.fontMetrics(c.style.fontSize),H=v.h,J=v.b,K,z;y&&(v={top:J,middle:J-H/2,bottom:-H+J},r?(x&&(c=C(c,{verticalAlign:"middle"})),w&&(c=C(c,{y:v[c.verticalAlign]}))):x?c=C(c,{verticalAlign:"top"}):w&&(c=C(c,{y:v[c.verticalAlign]})));j?j.translate(l,m):j=a.dataLabelsGroup=p.g("data-labels").attr({visibility:a.visible?"visible":"hidden",zIndex:6}).translate(l,m).add();h=c;n(f,function(f){K=f.dataLabel;c=h;(g=f.options)&&g.dataLabels&&(c=C(c,g.dataLabels)); -if(z=c.enabled){var l=f.barX&&f.barX+f.barW/2||o(f.plotX,-999),m=o(f.plotY,-999),n=c.y===null?f.y>=b.threshold?-H+J:J:c.y;d=(q?k.plotWidth-m:l)+c.x;e=t((q?k.plotHeight-l:m)+n)}if(K&&a.isCartesian&&(!k.isInsidePlot(d,e)||!z))f.dataLabel=K.destroy();else if(z){var l=c.align,v;i=c.formatter.call(f.getLabelConfig(),c);u==="column"&&(d+={left:-1,right:1}[l]*f.barW/2||0);!r&&q&&f.y<0&&(l="right",d-=10);c.style.color=o(c.color,c.style.color,a.color,"black");if(K)K.attr({text:i}).animate({x:d,y:e});else if(s(i)){l= -{align:l,fill:c.backgroundColor,stroke:c.borderColor,"stroke-width":c.borderWidth,r:c.borderRadius||0,rotation:c.rotation,padding:c.padding,zIndex:1};for(v in l)l[v]===A&&delete l[v];K=f.dataLabel=p[c.rotation?"text":"label"](i,d,e,null,null,null,c.useHTML,!0).attr(l).css(c.style).add(j).shadow(c.shadow)}if(y&&b.stacking&&K)v=f.barX,l=f.barY,m=f.barW,f=f.barH,K.align(c,null,{x:q?k.plotWidth-l-f:v,y:q?k.plotHeight-v-m:l,width:q?f:m,height:q?m:f})}})}},getSegmentPath:function(a){var b=this,c=[];n(a, -function(d,e){b.getPointSpline?c.push.apply(c,b.getPointSpline(a,d,e)):(c.push(e?"L":"M"),e&&b.options.step&&c.push(d.plotX,a[e-1].plotY),c.push(d.plotX,d.plotY))});return c},drawGraph:function(){var a=this,b=a.options,c=a.graph,d=[],e=a.group,f=b.lineColor||a.color,g=b.lineWidth,h=b.dashStyle,i,j=a.chart.renderer,k=[];n(a.segments,function(b){i=a.getSegmentPath(b);b.length>1?d=d.concat(i):k.push(b[0])});a.graphPath=d;a.singlePoints=k;if(c)Fa(c),c.animate({d:d});else if(g){c={stroke:f,"stroke-width":g}; -if(h)c.dashstyle=h;a.graph=j.path(d).attr(c).add(e).shadow(b.shadow)}},invertGroups:function(){function a(){var a={width:b.yAxis.len,height:b.xAxis.len};c.attr(a).invert();d&&d.attr(a).invert()}var b=this,c=b.group,d=b.trackerGroup,e=b.chart;I(e,"resize",a);I(b,"destroy",function(){P(e,"resize",a)});a();b.invertGroups=a},createGroup:function(){var a=this.chart;(this.group=a.renderer.g("series")).attr({visibility:this.visible?"visible":"hidden",zIndex:this.options.zIndex}).translate(this.xAxis.left, -this.yAxis.top).add(a.seriesGroup);this.createGroup=Sb},render:function(){var a=this,b=a.chart,c,d=a.options,e=d.clip!==!1,f=d.animation,f=(d=f&&a.animate)?f&&f.duration||500:0,g=a.clipRect,h=b.renderer;if(!g&&(g=a.clipRect=!b.hasRendered&&b.clipRect?b.clipRect:h.clipRect(0,0,b.plotSizeX,b.plotSizeY+1),!b.clipRect))b.clipRect=g;a.createGroup();c=a.group;a.drawDataLabels();d&&a.animate(!0);a.getAttribs();a.drawGraph&&a.drawGraph();a.drawPoints();a.options.enableMouseTracking!==!1&&a.drawTracker(); -b.inverted&&a.invertGroups();e&&!a.hasRendered&&(c.clip(g),a.trackerGroup&&a.trackerGroup.clip(b.clipRect));d&&a.animate();setTimeout(function(){g.isAnimating=!1;if((c=a.group)&&g!==b.clipRect&&g.renderer){if(e)c.clip(a.clipRect=b.clipRect);g.destroy()}},f);a.isDirty=a.isDirtyData=!1;a.hasRendered=!0},redraw:function(){var a=this.chart,b=this.isDirtyData,c=this.group;c&&(a.inverted&&c.attr({width:a.plotWidth,height:a.plotHeight}),c.animate({translateX:this.xAxis.left,translateY:this.yAxis.top})); -this.translate();this.setTooltipPoints(!0);this.render();b&&E(this,"updatedData")},setState:function(a){var b=this.options,c=this.graph,d=b.states,b=b.lineWidth,a=a||"";if(this.state!==a)this.state=a,d[a]&&d[a].enabled===!1||(a&&(b=d[a].lineWidth||b+1),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.points,j=c.options.chart.ignoreHiddenSeries;h=this.visible;h=(this.visible= -a=a===A?!h:a)?"show":"hide";if(e)e[h]();if(f)f[h]();else if(i)for(e=i.length;e--;)if(f=i[e],f.tracker)f.tracker[h]();if(g)g[h]();d&&c.legend.colorizeItem(this,a);this.isDirty=!0;this.options.stacking&&n(c.series,function(a){if(a.options.stacking&&a.visible)a.isDirty=!0});if(j)c.isDirtyBox=!0;b!==!1&&c.redraw();E(this,h)},show:function(){this.setVisible(!0)},hide:function(){this.setVisible(!1)},select:function(a){this.selected=a=a===A?!this.selected:a;if(this.checkbox)this.checkbox.checked=a;E(this, -a?"select":"unselect")},drawTrackerGroup:function(){var a=this.trackerGroup,b=this.chart;if(this.isCartesian){if(!a)this.trackerGroup=a=b.renderer.g().attr({zIndex:this.options.zIndex||1}).add(b.trackerGroup);a.translate(this.xAxis.left,this.yAxis.top)}return a},drawTracker:function(){var a=this,b=a.options,c=b.trackByArea,d=[].concat(c?a.areaPath:a.graphPath),e=d.length,f=a.chart,g=f.renderer,h=f.options.tooltip.snap,i=a.tracker,j=b.cursor,j=j&&{cursor:j},k=a.singlePoints,l=a.drawTrackerGroup(), -m;if(e&&!c)for(m=e+1;m--;)d[m]==="M"&&d.splice(m+1,0,d[m+1]-h,d[m+2],"L"),(m&&d[m]==="M"||m===e)&&d.splice(m,0,"L",d[m-2]+h,d[m-1]);for(m=0;m=0;d--)da&&i>e?(i=x(a,e),k=2*e-i):ig&&k>e?(k=x(g,e),i=2*e-k):kv?g-v:z-(f<=z?v:0));u(c,{barX:h,barY:i,barW:y,barH:j,pointWidth:r});c.shapeType="rect";c.shapeArgs=f=b.renderer.Element.prototype.crisp.call(0,e,h,i,y,j);e%2&&(f.y-=1,f.height+=1);c.trackerArgs=M(j)<3&&C(c.shapeArgs,{height:6,y:i-3})})},getSymbol:function(){},drawLegendSymbol:G.prototype.drawLegendSymbol,drawGraph:function(){},drawPoints:function(){var a=this,b=a.options,c=a.chart.renderer,d,e;n(a.points,function(f){var g=f.plotY;if(g!==A&&!isNaN(g)&& -f.y!==null)d=f.graphic,e=f.shapeArgs,d?(Fa(d),d.animate(C(e))):f.graphic=d=c[f.shapeType](e).attr(f.pointAttr[f.selected?"select":""]).add(a.group).shadow(b.shadow,null,b.stacking&&!b.borderRadius)})},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=a.drawTrackerGroup(),k,l,m;n(a.points,function(h){e=h.tracker;d=h.trackerArgs||h.shapeArgs;l=h.plotY;m=!a.isCartesian||l!==A&&!isNaN(l);delete d.strokeWidth;if(h.y!==null&&m)e?e.attr(d): -h.tracker=c[h.shapeType](d).attr({isTracker:f,fill:tb,visibility:a.visible?"visible":"hidden"}).on(ea?"touchstart":"mouseover",function(c){k=c.relatedTarget||c.fromElement;if(b.hoverSeries!==a&&z(k,"isTracker")!==f)a.onMouseOver();h.onMouseOver()}).on("mouseout",function(b){if(!g.stickyTracking&&(k=b.relatedTarget||b.toElement,z(k,"isTracker")!==f))a.onMouseOut()}).css(i).add(h.group||j)})},animate:function(a){var b=this,c=b.points,d=b.options;if(!a)n(c,function(a){var c=a.graphic,a=a.shapeArgs,g= -b.yAxis,h=d.threshold;c&&(c.attr({height:0,y:s(h)?g.getThreshold(h):g.translate(g.getExtremes().min,0,1,0,1)}),c.animate({height:a.height,y:a.y},d.animation))}),b.animate=null},remove:function(){var a=this,b=a.chart;b.hasRendered&&n(b.series,function(b){if(b.type===a.type)b.isDirty=!0});R.prototype.remove.apply(a,arguments)}});Y.column=ha;Z.bar=C(Z.column,{dataLabels:{align:"left",x:5,y:null,verticalAlign:"middle"}});Ia=ca(ha,{type:"bar",inverted:!0});Y.bar=Ia;Z.scatter=C(T,{lineWidth:0,states:{hover:{lineWidth:0}}, -tooltip:{headerFormat:'{series.name}
',pointFormat:"x: {point.x}
y: {point.y}
"}});Ia=ca(R,{type:"scatter",sorted:!1,translate:function(){var a=this;R.prototype.translate.apply(a);n(a.points,function(b){b.shapeType="circle";b.shapeArgs={x:b.plotX,y:b.plotY,r:a.chart.options.tooltip.snap}})},drawTracker:function(){for(var a=this,b=a.options.cursor,b=b&&{cursor:b},c=a.points,d=c.length,e;d--;)if(e=c[d].graphic)e.element._i= -d;a._hasTracking?a._hasTracking=!0:a.group.attr({isTracker:!0}).on(ea?"touchstart":"mouseover",function(b){a.onMouseOver();if(b.target._i!==A)c[b.target._i].onMouseOver()}).on("mouseout",function(){if(!a.options.stickyTracking)a.onMouseOut()}).css(b)}});Y.scatter=Ia;Z.pie=C(T,{borderColor:"#FFFFFF",borderWidth:1,center:["50%","50%"],colorByPoint:!0,dataLabels:{distance:30,enabled:!0,formatter:function(){return this.point.name},y:5},legendType:"point",marker:null,size:"75%",showInLegend:!1,slicedOffset:10, -states:{hover:{brightness:0.1,shadow:!1}}});T={type:"pie",isCartesian:!1,pointClass:ca(Wa,{init:function(){Wa.prototype.init.apply(this,arguments);var a=this,b;u(a,{visible:a.visible!==!1,name:o(a.name,"Slice")});b=function(){a.slice()};I(a,"select",b);I(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===A?!this.visible:a)?"show":"hide";this.group[g]();if(c)c[g]();if(d)d[g]();if(e)e[g](); -if(f)f[g]();this.legendItem&&b.legend.colorizeItem(this,a)},slice:function(a,b,c){var d=this.series.chart,e=this.slicedTranslation;ua(c,d);o(b,!0);a=this.sliced=s(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)}}),pointAttrToOptions:{stroke:"borderColor","stroke-width":"borderWidth",fill:"color"},getColor:function(){this.initialColor=this.chart.counters.color},animate:function(){var a=this;n(a.points, -function(b){var c=b.graphic,b=b.shapeArgs,d=-xa/2;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},setData:function(a,b){R.prototype.setData.call(this,a,!1);this.processData();this.generatePoints();o(b,!0)&&this.chart.redraw()},getCenter:function(){var a=this.options,b=this.chart,c=b.plotWidth,d=b.plotHeight,a=a.center.concat([a.size,a.innerSize||0]),e=O(c,d),f;return Ea(a,function(a,b){return(f=/%$/.test(a))?[c,d,e,e][b]*w(a)/100:a})}, -translate:function(){this.generatePoints();var a=0,b=-0.25,c=this.options,d=c.slicedOffset,e=d+c.borderWidth,f,g=this.chart,h,i,j,k=this.points,l=2*xa,m,p,o,s=c.dataLabels.distance;this.center=f=this.getCenter();this.getX=function(a,b){j=L.asin((a-f[1])/(f[2]/2+s));return f[0]+(b?-1:1)*X(j)*(f[2]/2+s)};n(k,function(b){a+=b.y});n(k,function(c){m=a?c.y/a:0;h=t(b*l*1E3)/1E3;b+=m;i=t(b*l*1E3)/1E3;c.shapeType="arc";c.shapeArgs={x:f[0],y:f[1],r:f[2]/2,innerR:f[3]/2,start:h,end:i};j=(i+h)/2;c.slicedTranslation= -Ea([X(j)*d+g.plotLeft,da(j)*d+g.plotTop],t);p=X(j)*f[2]/2;o=da(j)*f[2]/2;c.tooltipPos=[f[0]+p*0.7,f[1]+o*0.7];c.labelPos=[f[0]+p+X(j)*s,f[1]+o+da(j)*s,f[0]+p+X(j)*e,f[1]+o+da(j)*e,f[0]+p,f[1]+o,s<0?"center":j0,q=[[],[]],s,r,t,u,x=2,v;if(d.enabled){R.prototype.drawDataLabels.apply(this);n(a,function(a){a.dataLabel&&q[a.labelPos[7]0){for(v=m-l-j;v<=m+l+j;v+=a)w.push(v);t=w.length;if(A>t){h=[].concat(z);h.sort(u);for(v=A;v--;)h[v].rank=v;for(v=A;v--;)z[v].rank>=t&&z.splice(v,1);A=z.length}for(v=0;v -0){if(r=C.pop(),B=r.i,r=r.y,s>r&&w[B+1]!==null||s