highcharts
Version:
JavaScript charting framework
1,189 lines (1,188 loc) • 247 kB
JavaScript
/* *
*
* (c) 2010-2021 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import A from '../Animation/AnimationUtilities.js';
var animObject = A.animObject, setAnimation = A.setAnimation;
import H from '../Globals.js';
var hasTouch = H.hasTouch, svg = H.svg, win = H.win;
import LegendSymbolMixin from '../../Mixins/LegendSymbol.js';
import O from '../Options.js';
var defaultOptions = O.defaultOptions;
import palette from '../Color/Palette.js';
import Point from './Point.js';
import SeriesRegistry from './SeriesRegistry.js';
var seriesTypes = SeriesRegistry.seriesTypes;
import SVGElement from '../Renderer/SVG/SVGElement.js';
import U from '../Utilities.js';
var addEvent = U.addEvent, arrayMax = U.arrayMax, arrayMin = U.arrayMin, clamp = U.clamp, cleanRecursively = U.cleanRecursively, correctFloat = U.correctFloat, defined = U.defined, erase = U.erase, error = U.error, extend = U.extend, find = U.find, fireEvent = U.fireEvent, getNestedProperty = U.getNestedProperty, isArray = U.isArray, isFunction = U.isFunction, isNumber = U.isNumber, isString = U.isString, merge = U.merge, objectEach = U.objectEach, pick = U.pick, removeEvent = U.removeEvent, splat = U.splat, syncTimeout = U.syncTimeout;
/* *
*
* Class
*
* */
/**
* This is the base series prototype that all other series types inherit from.
* A new series is initialized either through the
* [series](https://api.highcharts.com/highcharts/series)
* option structure, or after the chart is initialized, through
* {@link Highcharts.Chart#addSeries}.
*
* The object can be accessed in a number of ways. All series and point event
* handlers give a reference to the `series` object. The chart object has a
* {@link Highcharts.Chart#series|series} property that is a collection of all
* the chart's series. The point objects and axis objects also have the same
* reference.
*
* Another way to reference the series programmatically is by `id`. Add an id
* in the series configuration options, and get the series object by
* {@link Highcharts.Chart#get}.
*
* Configuration options for the series are given in three levels. Options for
* all series in a chart are given in the
* [plotOptions.series](https://api.highcharts.com/highcharts/plotOptions.series)
* object. Then options for all series of a specific type
* are given in the plotOptions of that type, for example `plotOptions.line`.
* Next, options for one single series are given in the series array, or as
* arguments to `chart.addSeries`.
*
* 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 y values,
* equivalent to `series.data` and `series.points`.
*
* @class
* @name Highcharts.Series
*
* @param {Highcharts.Chart} chart
* The chart instance.
*
* @param {Highcharts.SeriesOptionsType|object} options
* The series options.
*/
var Series = /** @class */ (function () {
function Series() {
/* *
*
* Static Functions
*
* */
this._i = void 0;
this.chart = void 0;
this.data = void 0;
this.eventOptions = void 0;
this.eventsToUnbind = void 0;
this.index = void 0;
this.linkedSeries = void 0;
this.options = void 0;
this.points = void 0;
this.processedXData = void 0;
this.processedYData = void 0;
this.tooltipOptions = void 0;
this.userOptions = void 0;
this.xAxis = void 0;
this.yAxis = void 0;
this.zones = void 0;
/** eslint-enable valid-jsdoc */
}
/* *
*
* Functions
*
* */
/* eslint-disable valid-jsdoc */
Series.prototype.init = function (chart, options) {
fireEvent(this, 'init', { options: options });
var series = this, events, chartSeries = chart.series, lastSeries;
// A lookup over those events that are added by _options_ (not
// programmatically). These are updated through Series.update()
// (#10861).
this.eventOptions = this.eventOptions || {};
// The 'eventsToUnbind' property moved from prototype into the
// Series init to avoid reference to the same array between
// the different series and charts. #12959, #13937
this.eventsToUnbind = [];
/**
* Read only. The chart that the series belongs to.
*
* @name Highcharts.Series#chart
* @type {Highcharts.Chart}
*/
series.chart = chart;
/**
* Read only. The series' type, like "line", "area", "column" etc.
* The type in the series options anc can be altered using
* {@link Series#update}.
*
* @name Highcharts.Series#type
* @type {string}
*/
/**
* Read only. The series' current options. To update, use
* {@link Series#update}.
*
* @name Highcharts.Series#options
* @type {Highcharts.SeriesOptionsType}
*/
series.options = options = series.setOptions(options);
series.linkedSeries = [];
// bind the axes
series.bindAxes();
// set some variables
extend(series, {
/**
* The series name as given in the options. Defaults to
* "Series {n}".
*
* @name Highcharts.Series#name
* @type {string}
*/
name: options.name,
state: '',
/**
* Read only. The series' visibility state as set by {@link
* Series#show}, {@link Series#hide}, or in the initial
* configuration.
*
* @name Highcharts.Series#visible
* @type {boolean}
*/
visible: options.visible !== false,
/**
* Read only. The series' selected state as set by {@link
* Highcharts.Series#select}.
*
* @name Highcharts.Series#selected
* @type {boolean}
*/
selected: options.selected === true // false by default
});
// Register event listeners
events = options.events;
objectEach(events, function (event, eventType) {
if (isFunction(event)) {
// If event does not exist, or is changed by Series.update
if (series.eventOptions[eventType] !== event) {
// Remove existing if set by option
if (isFunction(series.eventOptions[eventType])) {
removeEvent(series, eventType, series.eventOptions[eventType]);
}
series.eventOptions[eventType] = event;
addEvent(series, eventType, event);
}
}
});
if ((events && events.click) ||
(options.point &&
options.point.events &&
options.point.events.click) ||
options.allowPointSelect) {
chart.runTrackerClick = true;
}
series.getColor();
series.getSymbol();
// Initialize the parallel data arrays
series.parallelArrays.forEach(function (key) {
if (!series[key + 'Data']) {
series[key + 'Data'] = [];
}
});
// Mark cartesian
if (series.isCartesian) {
chart.hasCartesianSeries = true;
}
// Get the index and register the series in the chart. The index is
// one more than the current latest series index (#5960).
if (chartSeries.length) {
lastSeries = chartSeries[chartSeries.length - 1];
}
series._i = pick(lastSeries && lastSeries._i, -1) + 1;
series.opacity = series.options.opacity;
// Insert the series and re-order all series above the insertion
// point.
chart.orderSeries(this.insert(chartSeries));
// Set options for series with sorting and set data later.
if (options.dataSorting && options.dataSorting.enabled) {
series.setDataSortingOptions();
}
else if (!series.points && !series.data) {
series.setData(options.data, false);
}
fireEvent(this, 'afterInit');
};
/**
* Check whether the series item is itself or inherits from a certain
* series type.
*
* @function Highcharts.Series#is
* @param {string} type The type of series to check for, can be either
* featured or custom series types. For example `column`, `pie`,
* `ohlc` etc.
*
* @return {boolean}
* True if this item is or inherits from the given type.
*/
Series.prototype.is = function (type) {
return seriesTypes[type] && this instanceof seriesTypes[type];
};
/**
* Insert the series in a collection with other series, either the chart
* series or yAxis series, in the correct order according to the index
* option. Used internally when adding series.
*
* @private
* @function Highcharts.Series#insert
* @param {Array<Highcharts.Series>} collection
* A collection of series, like `chart.series` or `xAxis.series`.
* @return {number}
* The index of the series in the collection.
*/
Series.prototype.insert = function (collection) {
var indexOption = this.options.index, i;
// Insert by index option
if (isNumber(indexOption)) {
i = collection.length;
while (i--) {
// Loop down until the interted element has higher index
if (indexOption >=
pick(collection[i].options.index, collection[i]._i)) {
collection.splice(i + 1, 0, this);
break;
}
}
if (i === -1) {
collection.unshift(this);
}
i = i + 1;
// Or just push it to the end
}
else {
collection.push(this);
}
return pick(i, collection.length - 1);
};
/**
* Set the xAxis and yAxis properties of cartesian series, and register
* the series in the `axis.series` array.
*
* @private
* @function Highcharts.Series#bindAxes
*/
Series.prototype.bindAxes = function () {
var series = this, seriesOptions = series.options, chart = series.chart, axisOptions;
fireEvent(this, 'bindAxes', null, function () {
// repeat for xAxis and yAxis
(series.axisTypes || []).forEach(function (AXIS) {
// loop through the chart's axis objects
chart[AXIS].forEach(function (axis) {
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 ||
(typeof seriesOptions[AXIS] !==
'undefined' &&
seriesOptions[AXIS] === axisOptions.id) ||
(typeof seriesOptions[AXIS] ===
'undefined' &&
axisOptions.index === 0)) {
// register this series in the axis.series lookup
series.insert(axis.series);
// set this series.xAxis or series.yAxis reference
/**
* Read only. The unique xAxis object associated
* with the series.
*
* @name Highcharts.Series#xAxis
* @type {Highcharts.Axis}
*/
/**
* Read only. The unique yAxis object associated
* with the series.
*
* @name Highcharts.Series#yAxis
* @type {Highcharts.Axis}
*/
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, chart);
}
});
});
fireEvent(this, 'afterBindAxes');
};
/**
* 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.
*
* @private
* @function Highcharts.Series#updateParallelArrays
*/
Series.prototype.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));
};
series.parallelArrays.forEach(fn);
};
/**
* Define hasData functions for series. These return true if there
* are data points on this series within the plot area.
*
* @private
* @function Highcharts.Series#hasData
* @return {boolean}
*/
Series.prototype.hasData = function () {
return ((this.visible &&
typeof this.dataMax !== 'undefined' &&
typeof this.dataMin !== 'undefined') || ( // #3703
this.visible &&
this.yData &&
this.yData.length > 0) // #9758
);
};
/**
* 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.
*
* @private
* @function Highcharts.Series#autoIncrement
* @return {number}
*/
Series.prototype.autoIncrement = function () {
var options = this.options, xIncrement = this.xIncrement, date, pointInterval, pointIntervalUnit = options.pointIntervalUnit, time = this.chart.time;
xIncrement = pick(xIncrement, options.pointStart, 0);
this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1);
// Added code for pointInterval strings
if (pointIntervalUnit) {
date = new time.Date(xIncrement);
if (pointIntervalUnit === 'day') {
time.set('Date', date, time.get('Date', date) + pointInterval);
}
else if (pointIntervalUnit === 'month') {
time.set('Month', date, time.get('Month', date) + pointInterval);
}
else if (pointIntervalUnit === 'year') {
time.set('FullYear', date, time.get('FullYear', date) + pointInterval);
}
pointInterval = date.getTime() - xIncrement;
}
this.xIncrement = xIncrement + pointInterval;
return xIncrement;
};
/**
* Internal function to set properties for series if data sorting is
* enabled.
*
* @private
* @function Highcharts.Series#setDataSortingOptions
*/
Series.prototype.setDataSortingOptions = function () {
var options = this.options;
extend(this, {
requireSorting: false,
sorted: false,
enabledDataSorting: true,
allowDG: false
});
// To allow unsorted data for column series.
if (!defined(options.pointRange)) {
options.pointRange = 1;
}
};
/**
* Set the series options by merging from the options tree. Called
* internally on initializing and updating series. This function will
* not redraw the series. For API usage, use {@link Series#update}.
* @private
* @function Highcharts.Series#setOptions
*
* @param {Highcharts.SeriesOptionsType} itemOptions
* The series options.
*
* @return {Highcharts.SeriesOptionsType}
*
* @fires Highcharts.Series#event:afterSetOptions
*/
Series.prototype.setOptions = function (itemOptions) {
var chart = this.chart, chartOptions = chart.options, plotOptions = chartOptions.plotOptions, userOptions = chart.userOptions || {}, seriesUserOptions = merge(itemOptions), options, zones, zone, styledMode = chart.styledMode, e = {
plotOptions: plotOptions,
userOptions: seriesUserOptions
};
fireEvent(this, 'setOptions', e);
// These may be modified by the event
var typeOptions = e.plotOptions[this.type], userPlotOptions = (userOptions.plotOptions || {});
// use copy to prevent undetected changes (#9762)
/**
* Contains series options by the user without defaults.
* @name Highcharts.Series#userOptions
* @type {Highcharts.SeriesOptionsType}
*/
this.userOptions = e.userOptions;
options = merge(typeOptions, plotOptions.series,
// #3881, chart instance plotOptions[type] should trump
// plotOptions.series
userOptions.plotOptions &&
userOptions.plotOptions[this.type], seriesUserOptions);
// The tooltip options are merged between global and series specific
// options. Importance order asscendingly:
// globals: (1)tooltip, (2)plotOptions.series,
// (3)plotOptions[this.type]
// init userOptions with possible later updates: 4-6 like 1-3 and
// (7)this series options
this.tooltipOptions = merge(defaultOptions.tooltip, // 1
defaultOptions.plotOptions.series &&
defaultOptions.plotOptions.series.tooltip, // 2
defaultOptions.plotOptions[this.type].tooltip, // 3
chartOptions.tooltip.userOptions, // 4
plotOptions.series &&
plotOptions.series.tooltip, // 5
plotOptions[this.type].tooltip, // 6
seriesUserOptions.tooltip // 7
);
// When shared tooltip, stickyTracking is true by default,
// unless user says otherwise.
this.stickyTracking = pick(seriesUserOptions.stickyTracking, userPlotOptions[this.type] &&
userPlotOptions[this.type].stickyTracking, userPlotOptions.series && userPlotOptions.series.stickyTracking, (this.tooltipOptions.shared && !this.noSharedTooltip ?
true :
options.stickyTracking));
// 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) {
zone = {
value: options[this.zoneAxis + 'Threshold'] ||
options.threshold ||
0,
className: 'highcharts-negative'
};
if (!styledMode) {
zone.color = options.negativeColor;
zone.fillColor = options.negativeFillColor;
}
zones.push(zone);
}
if (zones.length) { // Push one extra zone for the rest
if (defined(zones[zones.length - 1].value)) {
zones.push(styledMode ? {} : {
color: this.color,
fillColor: this.fillColor
});
}
}
fireEvent(this, 'afterSetOptions', { options: options });
return options;
};
/**
* Return series name in "Series {Number}" format or the one defined by
* a user. This method can be simply overridden as series name format
* can vary (e.g. technical indicators).
*
* @function Highcharts.Series#getName
*
* @return {string}
* The series name.
*/
Series.prototype.getName = function () {
// #4119
return pick(this.options.name, 'Series ' + (this.index + 1));
};
/**
* @private
* @function Highcharts.Series#getCyclic
*/
Series.prototype.getCyclic = function (prop, value, defaults) {
var i, chart = this.chart, userOptions = this.userOptions, indexName = prop + 'Index', counterName = prop + 'Counter', len = defaults ? defaults.length : pick(chart.options.chart[prop + 'Count'], chart[prop + 'Count']), setting;
if (!value) {
// Pick up either the colorIndex option, or the _colorIndex
// after Series.update()
setting = pick(userOptions[indexName], userOptions['_' + indexName]);
if (defined(setting)) { // after Series.update()
i = setting;
}
else {
// #6138
if (!chart.series.length) {
chart[counterName] = 0;
}
userOptions['_' + indexName] = i =
chart[counterName] % len;
chart[counterName] += 1;
}
if (defaults) {
value = defaults[i];
}
}
// Set the colorIndex
if (typeof i !== 'undefined') {
this[indexName] = i;
}
this[prop] = value;
};
/**
* Get the series' color based on either the options or pulled from
* global options.
*
* @private
* @function Highcharts.Series#getColor
*/
Series.prototype.getColor = function () {
if (this.chart.styledMode) {
this.getCyclic('color');
}
else if (this.options.colorByPoint) {
// #4359, selected slice got series.color even when colorByPoint
// was set.
this.options.color = null;
}
else {
this.getCyclic('color', this.options.color ||
defaultOptions.plotOptions[this.type].color, this.chart.options.colors);
}
};
/**
* Get all points' instances created for this series.
*
* @private
* @function Highcharts.Series#getPointsCollection
* @return {Array<Highcharts.Point>}
*/
Series.prototype.getPointsCollection = function () {
return (this.hasGroupedData ? this.points : this.data) || [];
};
/**
* Get the series' symbol based on either the options or pulled from
* global options.
*
* @private
* @function Highcharts.Series#getSymbol
* @return {void}
*/
Series.prototype.getSymbol = function () {
var seriesMarkerOption = this.options.marker;
this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols);
};
/**
* Finds the index of an existing point that matches the given point
* options.
*
* @private
* @function Highcharts.Series#findPointIndex
* @param {Highcharts.PointOptionsObject} optionsObject
* The options of the point.
* @param {number} fromIndex
* The index to start searching from, used for optimizing
* series with required sorting.
* @returns {number|undefined}
* Returns the index of a matching point, or undefined if no
* match is found.
*/
Series.prototype.findPointIndex = function (optionsObject, fromIndex) {
var id = optionsObject.id, x = optionsObject.x, oldData = this.points, matchingPoint, matchedById, pointIndex, matchKey, dataSorting = this.options.dataSorting;
if (id) {
matchingPoint = this.chart.get(id);
}
else if (this.linkedParent || this.enabledDataSorting) {
matchKey = (dataSorting && dataSorting.matchByName) ?
'name' : 'index';
matchingPoint = find(oldData, function (oldPoint) {
return !oldPoint.touched && oldPoint[matchKey] ===
optionsObject[matchKey];
});
// Add unmatched point as a new point
if (!matchingPoint) {
return void 0;
}
}
if (matchingPoint) {
pointIndex = matchingPoint && matchingPoint.index;
if (typeof pointIndex !== 'undefined') {
matchedById = true;
}
}
// Search for the same X in the existing data set
if (typeof pointIndex === 'undefined' && isNumber(x)) {
pointIndex = this.xData.indexOf(x, fromIndex);
}
// Reduce pointIndex if data is cropped
if (pointIndex !== -1 &&
typeof pointIndex !== 'undefined' &&
this.cropped) {
pointIndex = (pointIndex >= this.cropStart) ?
pointIndex - this.cropStart : pointIndex;
}
if (!matchedById &&
oldData[pointIndex] && oldData[pointIndex].touched) {
pointIndex = void 0;
}
return pointIndex;
};
/**
* Internal function called from setData. If the point count is the same
* as is was, or if there are overlapping X values, just run
* Point.update which is cheaper, allows animation, and keeps references
* to points. This also allows adding or removing points if the X-es
* don't match.
*
* @private
* @function Highcharts.Series#updateData
*/
Series.prototype.updateData = function (data, animation) {
var options = this.options, dataSorting = options.dataSorting, oldData = this.points, pointsToAdd = [], hasUpdatedByKey, i, point, lastIndex, requireSorting = this.requireSorting, equalLength = data.length === oldData.length, succeeded = true;
this.xIncrement = null;
// Iterate the new data
data.forEach(function (pointOptions, i) {
var id, x, pointIndex, optionsObject = (defined(pointOptions) &&
this.pointClass.prototype.optionsToObject.call({ series: this }, pointOptions)) || {};
// Get the x of the new data point
x = optionsObject.x;
id = optionsObject.id;
if (id || isNumber(x)) {
pointIndex = this.findPointIndex(optionsObject, lastIndex);
// Matching X not found
// or used already due to ununique x values (#8995),
// add point (but later)
if (pointIndex === -1 ||
typeof pointIndex === 'undefined') {
pointsToAdd.push(pointOptions);
// Matching X found, update
}
else if (oldData[pointIndex] &&
pointOptions !== options.data[pointIndex]) {
oldData[pointIndex].update(pointOptions, false, null, false);
// Mark it touched, below we will remove all points that
// are not touched.
oldData[pointIndex].touched = true;
// Speed optimize by only searching after last known
// index. Performs ~20% bettor on large data sets.
if (requireSorting) {
lastIndex = pointIndex + 1;
}
// Point exists, no changes, don't remove it
}
else if (oldData[pointIndex]) {
oldData[pointIndex].touched = true;
}
// If the length is equal and some of the nodes had a
// match in the same position, we don't want to remove
// non-matches.
if (!equalLength ||
i !== pointIndex ||
(dataSorting && dataSorting.enabled) ||
this.hasDerivedData) {
hasUpdatedByKey = true;
}
}
else {
// Gather all points that are not matched
pointsToAdd.push(pointOptions);
}
}, this);
// Remove points that don't exist in the updated data set
if (hasUpdatedByKey) {
i = oldData.length;
while (i--) {
point = oldData[i];
if (point && !point.touched && point.remove) {
point.remove(false, animation);
}
}
// If we did not find keys (ids or x-values), and the length is the
// same, update one-to-one
}
else if (equalLength && (!dataSorting || !dataSorting.enabled)) {
data.forEach(function (point, i) {
// .update doesn't exist on a linked, hidden series (#3709)
// (#10187)
if (oldData[i].update && point !== oldData[i].y) {
oldData[i].update(point, false, null, false);
}
});
// Don't add new points since those configs are used above
pointsToAdd.length = 0;
// Did not succeed in updating data
}
else {
succeeded = false;
}
oldData.forEach(function (point) {
if (point) {
point.touched = false;
}
});
if (!succeeded) {
return false;
}
// Add new points
pointsToAdd.forEach(function (point) {
this.addPoint(point, false, null, null, false);
}, this);
if (this.xIncrement === null &&
this.xData &&
this.xData.length) {
this.xIncrement = arrayMax(this.xData);
this.autoIncrement();
}
return true;
};
/**
* Apply a new set of data to the series and optionally redraw it. The
* new data array is passed by reference (except in case of
* `updatePoints`), and may later be mutated when updating the chart
* data.
*
* Note the difference in behaviour when setting the same amount of
* points, or a different amount of points, as handled by the
* `updatePoints` parameter.
*
* @sample highcharts/members/series-setdata/
* Set new data from a button
* @sample highcharts/members/series-setdata-pie/
* Set data in a pie
* @sample stock/members/series-setdata/
* Set new data in Highstock
* @sample maps/members/series-setdata/
* Set new data in Highmaps
*
* @function Highcharts.Series#setData
*
* @param {Array<Highcharts.PointOptionsType>} data
* Takes an array of data in the same format as described under
* `series.{type}.data` for the given series type, for example a
* line series would take data in the form described under
* [series.line.data](https://api.highcharts.com/highcharts/series.line.data).
*
* @param {boolean} [redraw=true]
* Whether to redraw the chart after the series is altered. If
* doing more operations on the chart, it is a good idea to set
* redraw to false and call {@link Chart#redraw} after.
*
* @param {boolean|Partial<Highcharts.AnimationOptionsObject>} [animation]
* When the updated data is the same length as the existing data,
* points will be updated by default, and animation visualizes
* how the points are changed. Set false to disable animation, or
* a configuration object to set duration or easing.
*
* @param {boolean} [updatePoints=true]
* When this is true, points will be updated instead of replaced
* whenever possible. This occurs a) when the updated data is the
* same length as the existing data, b) when points are matched
* by their id's, or c) when points can be matched by X values.
* This allows updating with animation and performs better. In
* this case, the original array is not passed by reference. Set
* `false` to prevent.
*/
Series.prototype.setData = function (data, redraw, animation, updatePoints) {
var series = this, oldData = series.points, oldDataLength = (oldData && oldData.length) || 0, dataLength, options = series.options, chart = series.chart, dataSorting = options.dataSorting, firstPoint = null, xAxis = series.xAxis, i, turboThreshold = options.turboThreshold, pt, xData = this.xData, yData = this.yData, pointArrayMap = series.pointArrayMap, valueCount = pointArrayMap && pointArrayMap.length, keys = options.keys, indexOfX = 0, indexOfY = 1, updatedData;
data = data || [];
dataLength = data.length;
redraw = pick(redraw, true);
if (dataSorting && dataSorting.enabled) {
data = this.sortData(data);
}
// First try to run Point.update which is cheaper, allows animation,
// and keeps references to points.
if (updatePoints !== false &&
dataLength &&
oldDataLength &&
!series.cropped &&
!series.hasGroupedData &&
series.visible &&
// Soft updating has no benefit in boost, and causes JS error
// (#8355)
!series.isSeriesBoosting) {
updatedData = this.updateData(data, animation);
}
if (!updatedData) {
// Reset properties
series.xIncrement = null;
series.colorCounter = 0; // for series with colorByPoint (#1547)
// Update parallel arrays
this.parallelArrays.forEach(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) {
firstPoint = series.getFirstValidPoint(data);
if (isNumber(firstPoint)) { // assume all points are numbers
for (i = 0; i < dataLength; i++) {
xData[i] = this.autoIncrement();
yData[i] = data[i];
}
// Assume all points are arrays when first point is
}
else if (isArray(firstPoint)) {
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]
if (keys) {
indexOfX = keys.indexOf('x');
indexOfY = keys.indexOf('y');
indexOfX = indexOfX >= 0 ? indexOfX : 0;
indexOfY = indexOfY >= 0 ? indexOfY : 1;
}
for (i = 0; i < dataLength; i++) {
pt = data[i];
xData[i] = pt[indexOfX];
yData[i] = pt[indexOfY];
}
}
}
else {
// Highcharts expects configs to be numbers or arrays in
// turbo mode
error(12, false, chart);
}
}
else {
for (i = 0; i < dataLength; i++) {
// stray commas in oldIE:
if (typeof data[i] !== 'undefined') {
pt = { series: series };
series.pointClass.prototype.applyOptions.apply(pt, [data[i]]);
series.updateParallelArrays(pt, i);
}
}
}
// Forgetting to cast strings to numbers is a common caveat when
// handling CSV or JSON
if (yData && isString(yData[0])) {
error(14, true, chart);
}
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 = chart.isDirtyBox = true;
series.isDirtyData = !!oldData;
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);
}
};
/**
* Internal function to sort series data
*
* @private
* @function Highcharts.Series#sortData
*
* @param {Array<Highcharts.PointOptionsType>} data
* Force data grouping.
*
* @return {Array<Highcharts.PointOptionsObject>}
*/
Series.prototype.sortData = function (data) {
var series = this, options = series.options, dataSorting = options.dataSorting, sortKey = dataSorting.sortKey || 'y', sortedData, getPointOptionsObject = function (series, pointOptions) {
return (defined(pointOptions) &&
series.pointClass.prototype.optionsToObject.call({
series: series
}, pointOptions)) || {};
};
data.forEach(function (pointOptions, i) {
data[i] = getPointOptionsObject(series, pointOptions);
data[i].index = i;
}, this);
// Sorting
sortedData = data.concat().sort(function (a, b) {
var aValue = getNestedProperty(sortKey, a);
var bValue = getNestedProperty(sortKey, b);
return bValue < aValue ? -1 : bValue > aValue ? 1 : 0;
});
// Set x value depending on the position in the array
sortedData.forEach(function (point, i) {
point.x = i;
}, this);
// Set the same x for linked series points if they don't have their
// own sorting
if (series.linkedSeries) {
series.linkedSeries.forEach(function (linkedSeries) {
var options = linkedSeries.options, seriesData = options.data;
if ((!options.dataSorting ||
!options.dataSorting.enabled) &&
seriesData) {
seriesData.forEach(function (pointOptions, i) {
seriesData[i] = getPointOptionsObject(linkedSeries, pointOptions);
if (data[i]) {
seriesData[i].x = data[i].x;
seriesData[i].index = i;
}
});
linkedSeries.setData(seriesData, false);
}
});
}
return data;
};
/**
* Internal function to process the data by cropping away unused data
* points if the series is longer than the crop threshold. This saves
* computing time for large series.
*
* @private
* @function Highcharts.Series#getProcessedData
* @param {boolean} [forceExtremesFromAll]
* Force getting extremes of a total series data range.
* @return {Highcharts.SeriesProcessedDataObject}
*/
Series.prototype.getProcessedData = function (forceExtremesFromAll) {
var series = this,
// copied during slice operation:
processedXData = series.xData, 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 = forceExtremesFromAll ||
series.getExtremesFromAll ||
options.getExtremesFromAll, // #4599
isCartesian = series.isCartesian, xExtremes, val2lin = xAxis && xAxis.val2lin, isLog = !!(xAxis && xAxis.logarithmic), throwOnUnsorted = series.requireSorting, min, max;
if (xAxis) {
// corrected for log axis (#3053)
xExtremes = xAxis.getExtremes();
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 (series.yData && (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 &&
(typeof 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 && throwOnUnsorted) {
error(15, false, series.chart);
throwOnUnsorted = false; // Only once
}
}
return {
xData: processedXData,
yData: processedYData,
cropped: cropped,
cropStart: cropStart,
closestPointRange: closestPointRange
};
};
/**
* Internal function to apply processed data.
* In Highstock, this function is extended to provide data grouping.
*
* @private
* @function Highcharts.Series#processData
* @param {boolean} [force]
* Force data grouping.
* @return {boolean|undefined}
*/
Series.prototype.processData = function (force) {
var series = this, xAxis = series.xAxis, processedData;
// 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 (series.isCartesian &&
!series.isDirty &&
!xAxis.isDirty &&
!series.yAxis.isDirty &&
!force) {
return false;
}
processedData = series.getProcessedData();
// Record the properties
series.cropped = processedData.cropped; // undefined or true
series.cropStart = processedData.cropStart;
series.processedXData = processedData.xData;
series.processedYData = processedData.yData;
series.closestPointRange = series.basePointRange = processedData.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.
*
* @private
* @function Highcharts.Series#cropData
* @param {Array<number>} xData
* @param {Array<number>} yData
* @param {number} min
* @param {number} max
* @param {number} [cropShoulder]
* @return {Highcharts.SeriesCropDataObject}
*/
Series.prototype.cropData = function (xData, yData, min, max, cropShoulder) {
var dataLength = xData.length, cropStart = 0, cropEnd = dataLength, i, j;
// line-type series need one point outside
cropShoulder = pick(cropShoulder, this.cropShoulder);
// iterate up to find slice start
for (i = 0; i < dataLength; i++) {
if (xData[i] >= min) {
cropStart = Math.max(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.
*
* @private
* @function Highcharts.Series#generatePoints
*/
Series.prototype.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, keys = options.keys, point, points = [], i;
if (!data && !hasGroupedData) {
var arr = [];
arr.length = dataOptions.length;
data = series.data = arr;
}
if (keys && hasGroupedData) {
// grouped data has already applied keys (#6590)
series.options.keys = false;
}
for (i = 0; i < processedDataLength; i++) {
cursor = cropStart + i;
if (!hasGroupedData) {
point = data[cursor];
// #970:
if (!point &&
typeof dataOptions[cursor] !== 'undefined') {
data[cursor] = point = (new PointClass()).init(series, dataOptions[cursor], processedXData[i]);
}
}
else {
// splat the y data in case of ohlc data array
point = (new PointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i])));
/**
* Highstock only. If a point object is created by data
* grouping, it doesn't reflect actual points in the raw
* data. In this case, the `dataGroup` property holds
* information that points back to the raw data.
*
* - `dataGroup.start` is the index of the first raw data
* point in the group.
*
* - `dataGroup.length` is the amount of points in the
* group.
*
* @product highstock
*
* @name Highcharts.Point#dataGroup
* @type {Highcharts.DataGroupingInfoObject|undefined}
*/
point.dataGroup = series.groupMap[i];
if (point.dataGroup.options) {
point.options = point.dataGroup.options;