UNPKG

highcharts

Version:
1,189 lines (1,188 loc) 247 kB
/* * * * (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;