highcharts
Version:
JavaScript charting framework
1,204 lines • 148 kB
JavaScript
/* *
*
* (c) 2010-2025 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import A from '../Animation/AnimationUtilities.js';
const { animObject, setAnimation } = A;
import DataTableCore from '../../Data/DataTableCore.js';
import D from '../Defaults.js';
const { defaultOptions } = D;
import F from '../Foundation.js';
const { registerEventOptions } = F;
import H from '../Globals.js';
const { svg, win } = H;
import LegendSymbol from '../Legend/LegendSymbol.js';
import Point from './Point.js';
import SeriesDefaults from './SeriesDefaults.js';
import SeriesRegistry from './SeriesRegistry.js';
const { seriesTypes } = SeriesRegistry;
import SVGElement from '../Renderer/SVG/SVGElement.js';
import T from '../Templating.js';
const { format } = T;
import U from '../Utilities.js';
const { arrayMax, arrayMin, clamp, correctFloat, crisp, defined, destroyObjectProperties, diffObjects, erase, error, extend, find, fireEvent, getClosestDistance, getNestedProperty, insertItem, isArray, isNumber, isString, merge, objectEach, pick, removeEvent, syncTimeout } = U;
/* *
*
* 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`.
*
* - The `series.dataTable` refers to an instance of [DataTableCore](https://api.highcharts.com/class-reference/Highcharts.Data)
* or `DataTable` that contains the data in a tabular format. Individual
* columns can be read from `series.getColumn()`.
*
* - 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.
*
* @class
* @name Highcharts.Series
*
* @param {Highcharts.Chart} chart
* The chart instance.
*
* @param {Highcharts.SeriesOptionsType|object} options
* The series options.
*/
class Series {
constructor() {
/* *
*
* Static Properties
*
* */
this.zoneAxis = 'y';
// eslint-enable valid-jsdoc
}
/* *
*
* Functions
*
* */
/* eslint-disable valid-jsdoc */
init(chart, userOptions) {
fireEvent(this, 'init', { options: userOptions });
// Create the data table
this.dataTable ?? (this.dataTable = new DataTableCore());
const series = this, chartSeries = chart.series;
// 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 = series.setOptions(userOptions);
const options = series.options, visible = options.visible !== false;
/**
* All child series that are linked to the current series through the
* [linkedTo](https://api.highcharts.com/highcharts/series.line.linkedTo)
* option.
*
* @name Highcharts.Series#linkedSeries
* @type {Array<Highcharts.Series>}
* @readonly
*/
series.linkedSeries = [];
// Bind the axes
series.bindAxes();
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, // True by default
/**
* 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
});
registerEventOptions(this, options);
const events = options.events;
if (events?.click ||
options.point?.events?.click ||
options.allowPointSelect) {
chart.runTrackerClick = true;
}
series.getColor();
series.getSymbol();
// 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).
let lastSeries;
if (chartSeries.length) {
lastSeries = chartSeries[chartSeries.length - 1];
}
series._i = pick(lastSeries?._i, -1) + 1;
series.opacity = series.options.opacity;
// Insert the series and re-order all series above the insertion
// point.
chart.orderItems('series', insertItem(this, chartSeries));
// Set options for series with sorting and set data later.
if (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.
*/
is(type) {
return seriesTypes[type] && this instanceof seriesTypes[type];
}
/**
* Set the xAxis and yAxis properties of cartesian series, and register
* the series in the `axis.series` array.
*
* @private
* @function Highcharts.Series#bindAxes
*/
bindAxes() {
const series = this, seriesOptions = series.options, chart = series.chart;
let axisOptions;
fireEvent(this, 'bindAxes', null, function () {
// Repeat for xAxis and yAxis
(series.axisTypes || []).forEach(function (coll) {
// Loop through the chart's axis objects
(chart[coll] || []).forEach(function (axis) {
axisOptions = axis.options;
// Apply if the series xAxis or yAxis option matches
// the number of the axis, or if undefined, use the
// first axis
if (pick(seriesOptions[coll], 0) === axis.index ||
(typeof seriesOptions[coll] !==
'undefined' &&
seriesOptions[coll] === axisOptions.id)) {
// Register this series in the axis.series lookup
insertItem(series, 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[coll] = axis;
// Mark dirty for redraw
axis.isDirty = true;
}
});
// The series needs an X and an Y axis
if (!series[coll] &&
series.optionalAxis !== coll) {
error(18, true, chart);
}
});
});
fireEvent(this, 'afterBindAxes');
}
/**
* 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
*/
hasData() {
return ((this.visible &&
typeof this.dataMax !== 'undefined' &&
typeof this.dataMin !== 'undefined') || ( // #3703
this.visible &&
this.dataTable.rowCount > 0 // #9758
));
}
/**
* Determine whether the marker in a series has changed.
*
* @private
* @function Highcharts.Series#hasMarkerChanged
*/
hasMarkerChanged(options, oldOptions) {
const marker = options.marker, oldMarker = oldOptions.marker || {};
return marker && ((oldMarker.enabled && !marker.enabled) ||
oldMarker.symbol !== marker.symbol || // #10870, #15946
oldMarker.height !== marker.height || // #16274
oldMarker.width !== marker.width // #16274
);
}
/**
* 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
*/
autoIncrement(x) {
const options = this.options, { pointIntervalUnit, relativeXValue } = this.options, time = this.chart.time, xIncrement = this.xIncrement ??
time.parse(options.pointStart) ??
0;
let pointInterval;
this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1);
if (relativeXValue && isNumber(x)) {
pointInterval *= x;
}
// Added code for pointInterval strings
if (pointIntervalUnit) {
const d = time.toParts(xIncrement);
if (pointIntervalUnit === 'day') {
d[2] += pointInterval;
}
else if (pointIntervalUnit === 'month') {
d[1] += pointInterval;
}
else if (pointIntervalUnit === 'year') {
d[0] += pointInterval;
}
pointInterval = time.makeTime.apply(time, d) - xIncrement;
}
if (relativeXValue && isNumber(x)) {
return xIncrement + pointInterval;
}
this.xIncrement = xIncrement + pointInterval;
return xIncrement;
}
/**
* Internal function to set properties for series if data sorting is
* enabled.
*
* @private
* @function Highcharts.Series#setDataSortingOptions
*/
setDataSortingOptions() {
const 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.
* @emits Highcharts.Series#event:afterSetOptions
*/
setOptions(itemOptions) {
const chart = this.chart, chartOptions = chart.options, plotOptions = chartOptions.plotOptions, userOptions = chart.userOptions || {}, seriesUserOptions = merge(itemOptions), styledMode = chart.styledMode, e = {
plotOptions: plotOptions,
userOptions: seriesUserOptions
};
let zone;
fireEvent(this, 'setOptions', e);
// These may be modified by the event
const typeOptions = e.plotOptions[this.type], userPlotOptions = (userOptions.plotOptions || {}), userPlotOptionsSeries = userPlotOptions.series || {}, defaultPlotOptionsType = (defaultOptions.plotOptions[this.type] || {}), userPlotOptionsType = userPlotOptions[this.type] || {};
// Merge in multiple data label options from the plot option. (#21928)
typeOptions.dataLabels = this.mergeArrays(defaultPlotOptionsType.dataLabels, typeOptions.dataLabels);
// 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;
const options = merge(typeOptions, plotOptions.series,
// #3881, chart instance plotOptions[type] should trump
// plotOptions.series
userPlotOptionsType, seriesUserOptions);
// The tooltip options are merged between global and series specific
// options. Importance order ascendingly:
// 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?.tooltip, // 2
defaultPlotOptionsType?.tooltip, // 3
chart.userOptions.tooltip, // 4
userPlotOptions.series?.tooltip, // 5
userPlotOptionsType.tooltip, // 6
seriesUserOptions.tooltip // 7
);
// When shared tooltip, stickyTracking is true by default,
// unless user says otherwise.
this.stickyTracking = pick(seriesUserOptions.stickyTracking, userPlotOptionsType.stickyTracking, userPlotOptionsSeries.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 || 'y';
const zones = this.zones = // #20440, create deep copy of zones options
(options.zones || []).map((z) => ({ ...z }));
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);
}
// Push one extra zone for the rest
if (zones.length && 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.
*/
getName() {
// #4119
return this.options.name ??
format(this.chart.options.lang.seriesName, this, this.chart);
}
/**
* @private
* @function Highcharts.Series#getCyclic
*/
getCyclic(prop, value, defaults) {
const chart = this.chart, indexName = `${prop}Index`, counterName = `${prop}Counter`, len = (
// Symbol count
defaults?.length ||
// Color count
chart.options.chart.colorCount);
let i, setting;
if (!value) {
// Pick up either the colorIndex option, or the series.colorIndex
// after Series.update()
setting = pick(prop === 'color' ? this.options.colorIndex : void 0, this[indexName]);
if (defined(setting)) { // After Series.update()
i = setting;
}
else {
// #6138
if (!chart.series.length) {
chart[counterName] = 0;
}
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
*/
getColor() {
if (this.chart.styledMode) {
this.getCyclic('color');
}
else if (this.options.colorByPoint) {
this.color = "#cccccc" /* Palette.neutralColor20 */;
}
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
*/
getPointsCollection() {
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
*/
getSymbol() {
const seriesMarkerOption = this.options.marker;
this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols);
}
/**
* Shorthand to get one of the series' data columns from `Series.dataTable`.
*
* @private
* @function Highcharts.Series#getColumn
*/
getColumn(columnName, modified) {
return (modified ? this.dataTable.modified : this.dataTable)
.getColumn(columnName, true) || [];
}
/**
* 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.
* @return {number|undefined}
* Returns the index of a matching point, or undefined if no match is found.
*/
findPointIndex(optionsObject, fromIndex) {
const { id, x } = optionsObject, oldData = this.points, dataSorting = this.options.dataSorting, cropStart = this.cropStart || 0;
let matchingPoint, matchedById, pointIndex;
if (id) {
const item = this.chart.get(id);
if (item instanceof Point) {
matchingPoint = item;
}
}
else if (this.linkedParent ||
this.enabledDataSorting ||
this.options.relativeXValue) {
let matcher = (oldPoint) => !oldPoint.touched &&
oldPoint.index === optionsObject.index;
if (dataSorting?.matchByName) {
matcher = (oldPoint) => !oldPoint.touched &&
oldPoint.name === optionsObject.name;
}
else if (this.options.relativeXValue) {
matcher = (oldPoint) => !oldPoint.touched &&
oldPoint.options.x === optionsObject.x;
}
matchingPoint = find(oldData, matcher);
// Add unmatched point as a new point
if (!matchingPoint) {
return void 0;
}
}
if (matchingPoint) {
pointIndex = 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.getColumn('x').indexOf(x, fromIndex);
}
// Reduce pointIndex if data is cropped
if (pointIndex !== -1 &&
typeof pointIndex !== 'undefined' &&
this.cropped) {
pointIndex = pointIndex >= cropStart ?
pointIndex - cropStart : pointIndex;
}
if (!matchedById &&
isNumber(pointIndex) &&
oldData[pointIndex]?.touched) {
pointIndex = void 0;
}
return pointIndex;
}
/**
* Internal function called from setData. If the point count is the same
* as it 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
*/
updateData(data, animation) {
const { options, requireSorting } = this, dataSorting = options.dataSorting, oldData = this.points, pointsToAdd = [], equalLength = data.length === oldData.length;
let hasUpdatedByKey, i, point, lastIndex, succeeded = true;
this.xIncrement = null;
// Iterate the new data
data.forEach((pointOptions, i) => {
const optionsObject = (defined(pointOptions) &&
this.pointClass.prototype.optionsToObject.call({ series: this }, pointOptions)) || {}, { id, x } = optionsObject;
let pointIndex;
if (id || isNumber(x)) {
pointIndex = this.findPointIndex(optionsObject, lastIndex);
// Matching X not found or used already due to non-unique 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, void 0, 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% better 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?.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?.(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?.enabled) {
data.forEach((point, i) => {
// .update doesn't exist on a linked, hidden series (#3709)
// (#10187)
if (point !== oldData[i].y && !oldData[i].destroyed) {
oldData[i].update(point, false, void 0, 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((point) => {
if (point) {
point.touched = false;
}
});
if (!succeeded) {
return false;
}
// Add new points
pointsToAdd.forEach((point) => {
this.addPoint(point, false, void 0, void 0, false);
}, this);
const xData = this.getColumn('x');
if (this.xIncrement === null &&
xData.length) {
this.xIncrement = arrayMax(xData);
this.autoIncrement();
}
return true;
}
dataColumnKeys() {
return ['x', ...(this.pointArrayMap || ['y'])];
}
/**
* 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 Highcharts Stock
* @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.
*/
setData(data, redraw = true, animation, updatePoints) {
const series = this, oldData = series.points, oldDataLength = oldData?.length || 0, options = series.options, chart = series.chart, dataSorting = options.dataSorting, xAxis = series.xAxis, turboThreshold = options.turboThreshold, table = this.dataTable, dataColumnKeys = this.dataColumnKeys(), pointValKey = series.pointValKey || 'y', pointArrayMap = series.pointArrayMap || [], valueCount = pointArrayMap.length, keys = options.keys;
let i, updatedData, indexOfX = 0, indexOfY = 1, copiedData;
if (!chart.options.chart.allowMutatingData) { // #4259
// Remove old reference
if (options.data) {
delete series.options.data;
}
if (series.userOptions.data) {
delete series.userOptions.data;
}
copiedData = merge(true, data);
}
data = copiedData || data || [];
const dataLength = data.length;
if (dataSorting?.enabled) {
data = this.sortData(data);
}
// First try to run Point.update which is cheaper, allows animation, and
// keeps references to points.
if (chart.options.chart.allowMutatingData &&
updatePoints !== false &&
dataLength &&
oldDataLength &&
!series.cropped &&
!series.hasGroupedData &&
series.visible &&
// Soft updating has no benefit in boost, and causes JS error
// (#8355)
!series.boosted) {
updatedData = this.updateData(data, animation);
}
if (!updatedData) {
// Reset properties
series.xIncrement = null;
series.colorCounter = 0; // For series with colorByPoint (#1547)
// In turbo mode, look for one- or twodimensional arrays of numbers.
// The first and the last valid value are 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.
let runTurbo = turboThreshold && dataLength > turboThreshold;
if (runTurbo) {
const firstPoint = series.getFirstValidPoint(data), lastPoint = series.getFirstValidPoint(data, dataLength - 1, -1), isShortArray = (a) => Boolean(isArray(a) && (keys || isNumber(a[0])));
// Assume all points are numbers
if (isNumber(firstPoint) && isNumber(lastPoint)) {
const x = [], valueData = [];
for (const value of data) {
x.push(this.autoIncrement());
valueData.push(value);
}
table.setColumns({
x,
[pointValKey]: valueData
});
// Assume all points are arrays when first point is
}
else if (isShortArray(firstPoint) &&
isShortArray(lastPoint)) {
if (valueCount) { // [x, low, high] or [x, o, h, l, c]
// When autoX is 1, the x is skipped: [low, high]. When
// autoX is 0, the x is included: [x, low, high]
const autoX = firstPoint.length === valueCount ?
1 : 0, colArray = new Array(dataColumnKeys.length)
.fill(0).map(() => []);
for (const pt of data) {
if (autoX) {
colArray[0].push(this.autoIncrement());
}
for (let j = autoX; j <= valueCount; j++) {
colArray[j]?.push(pt[j - autoX]);
}
}
table.setColumns(dataColumnKeys.reduce((columns, columnName, i) => {
columns[columnName] = colArray[i];
return columns;
}, {}));
}
else { // [x, y]
if (keys) {
indexOfX = keys.indexOf('x');
indexOfY = keys.indexOf('y');
indexOfX = indexOfX >= 0 ? indexOfX : 0;
indexOfY = indexOfY >= 0 ? indexOfY : 1;
}
if (firstPoint.length === 1) {
indexOfY = 0;
}
const xData = [], valueData = [];
if (indexOfX === indexOfY) {
for (const pt of data) {
xData.push(this.autoIncrement());
valueData.push(pt[indexOfY]);
}
}
else {
for (const pt of data) {
xData.push(pt[indexOfX]);
valueData.push(pt[indexOfY]);
}
}
table.setColumns({
x: xData,
[pointValKey]: valueData
});
}
}
else {
// Highcharts expects configs to be numbers or arrays in
// turbo mode
runTurbo = false;
}
}
if (!runTurbo) {
const columns = dataColumnKeys.reduce((columns, columnName) => {
columns[columnName] = [];
return columns;
}, {});
for (i = 0; i < dataLength; i++) {
const pt = series.pointClass.prototype.applyOptions.apply({ series }, [data[i]]);
for (const key of dataColumnKeys) {
columns[key][i] = pt[key];
}
}
table.setColumns(columns);
}
// Forgetting to cast strings to numbers is a common caveat when
// handling CSV or JSON
if (isString(this.getColumn('y')[0])) {
error(14, true, chart);
}
series.data = [];
series.options.data = series.userOptions.data = data;
// Destroy old points
i = oldDataLength;
while (i--) {
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.
*/
sortData(data) {
const series = this, options = series.options, dataSorting = options.dataSorting, sortKey = dataSorting.sortKey || 'y', 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
const sortedData = data.concat().sort((a, b) => {
const aValue = getNestedProperty(sortKey, a);
const 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) {
const options = linkedSeries.options, seriesData = options.data;
if (!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.
*/
getProcessedData(forceExtremesFromAll) {
const series = this, { dataTable: table, isCartesian, options, xAxis } = series, cropThreshold = options.cropThreshold, getExtremesFromAll = forceExtremesFromAll ||
// X-range series etc, #21003
series.getExtremesFromAll, logarithmic = xAxis?.logarithmic, dataLength = table.rowCount;
let croppedData, cropped, cropStart = 0, xExtremes, min, max, xData = series.getColumn('x'), modified = table, updatingNames = false;
if (xAxis) {
// Corrected for log axis (#3053)
xExtremes = xAxis.getExtremes();
min = xExtremes.min;
max = xExtremes.max;
updatingNames = !!(xAxis.categories && !xAxis.names.length);
// Optionally filter out points outside the plot area
if (isCartesian &&
series.sorted &&
!getExtremesFromAll &&
(!cropThreshold ||
dataLength > cropThreshold ||
series.forceCrop)) {
// It's outside current extremes
if (xData[dataLength - 1] < min ||
xData[0] > max) {
modified = new DataTableCore();
// Only crop if it's actually spilling out
}
else if (
// Don't understand why this condition is needed
series.getColumn(series.pointValKey || 'y').length && (xData[0] < min ||
xData[dataLength - 1] > max)) {
croppedData = this.cropData(table, min, max);
modified = croppedData.modified;
cropStart = croppedData.start;
cropped = true;
}
}
}
// Find the closest distance between processed points
xData = modified.getColumn('x') || [];
const closestPointRange = getClosestDistance([
logarithmic ?
xData.map(logarithmic.log2lin) :
xData
],
// 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). Avoid warning during the
// premature processing pass in updateNames (#16104).
() => (series.requireSorting &&
!updatingNames &&
error(15, false, series.chart)));
return {
modified,
cropped,
cropStart,
closestPointRange
};
}
/**
* Internal function to apply processed data.
* In Highcharts Stock, this function is extended to provide data grouping.
*
* @private
* @function Highcharts.Series#processData
* @param {boolean} [force]
* Force data grouping.
*/
processData(force) {
const series = this, xAxis = series.xAxis, table = series.dataTable;
// 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;
}
const processedData = series.getProcessedData();
// Record the properties
table.modified = processedData.modified;
series.cropped = processedData.cropped; // Undefined or true
series.cropStart = processedData.cropStart;
series.closestPointRange = (series.basePointRange = processedData.closestPointRange);
fireEvent(series, 'afterProcessData');
}
/**
* 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
*/
cropData(table, min, max) {
const xData = table.getColumn('x', true) || [], dataLength = xData.length, columns = {};
let i, j, start = 0, end = dataLength;
// Iterate up to find slice start
for (i = 0; i < dataLength; i++) {
if (xData[i] >= min) {
start = Math.max(0, i - 1);
break;
}
}
// Proceed to find slice end
for (j = i; j < dataLength; j++) {
if (xData[j] > max) {
end = j + 1;
break;
}
}
for (const key of this.dataColumnKeys()) {
const column = table.getColumn(key, true);
if (column) {
columns[key] = column.slice(start, end);
}
}
return {
modified: new DataTableCore({ columns }),
start,
end
};
}
/**
* 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
*/
generatePoints() {
const series = this, options = series.options, dataOptions = series.processedData || options.data, table = series.dataTable.modified, xData = series.getColumn('x', true), PointClass = series.pointClass, processedDataLength = table.rowCount, cropStart = series.cropStart || 0, hasGroupedData = series.hasGroupedData, keys = options.keys, points = [], groupCropStartIndex = (options.dataGrouping?.groupAll ?
cropStart :
0), categories = series.xAxis?.categories, pointArrayMap = series.pointArrayMap || ['y'],
// Create a configuration object out of a data row
dataColumnKeys = this.dataColumnKeys();
let dataLength, cursor, point, i, data = series.data, pOptions;
if (!data && !hasGroupedData) {
const arr = [];
arr.length = dataOptions?.length || 0;
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];
pOptions = dataOptions ?
dataOptions[cursor] :
table.getRow(i, pointArrayMap);
// #970:
if (!point &&
pOptions !== void 0) {
data[cursor] = point = new PointClass(series, pOptions, xData[i]);
}
}
else {
// Splat the y data in case of ohlc data array
point = new PointClass(series, table.getRow(i, dataColumnKeys) || []);
point.dataGroup = series.groupMap[groupCropStartIndex + i];
if (point.dataGroup?.options) {
point.options = point.dataGroup.options;
extend(point, point.dataGroup.options);
// Collision of props and options (#9770)
delete point.dataLabels;
}
}
if (point) { // #6279
/**
* Contains the point's index in the `Series.points` array.
*
* @name Highcharts.Point#index
* @type {number}
* @readonly
*/
// For faster access in Point.update
point.index = hasGroupedData ?
(groupCropStartIndex + i) : cursor;
points[i] = point;
// Set point properties for convenient access in tooltip and
// data labels
point.category = categories?.[point.x] ?? point.x;
point.key = point.name ?? point.category;
}
}
// Restore keys options (#6590)
series.options.keys = keys;
// Hide cropped-away points - this only runs when the number of
// points is above cropThreshold, or when switching view from
// non-grouped data to grouped data (#637)
if (data &&
(processedDataLength !== (dataLength = data.length) ||
hasGroupedData)) {
for (i = 0; i < dataLength; i++) {
// When has grouped data, clear all points
if (i === cropStart && !hasGroupedData) {
i += processedDataLength;
}
if (data[i]) {
data[i].destroyElements();
data[i].plotX = void 0; // #1003
}
}
}
/**
* Read only. An array containing those values converted to points.
* In case the series data length exceeds the `cropThreshold`, or if
* the data is grouped, `series.data` doesn't contain all the
* points. Also, in case a series is hidden, the `data` array may be
* empty. In case of cropping, the `data` array may contain `undefined`
* values, instead of points. To access raw values,
* `series.options.data` will always be up to date. `Series.data` only
* contains the points that have been created on demand. To modify the
* data, use
* {@link Highcharts.Series#setData} or
*