UNPKG

highcharts

Version:
1,204 lines 148 kB
/* * * * (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 *