/* * * * (c) 2010-2020 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import Axis from './Axis.js'; import H from './Globals.js'; import Point from './Point.js'; import U from './Utilities.js'; var addEvent = U.addEvent, arrayMax = U.arrayMax, arrayMin = U.arrayMin, clamp = U.clamp, defined = U.defined, extend = U.extend, find = U.find, format = U.format, isNumber = U.isNumber, isString = U.isString, merge = U.merge, pick = U.pick, splat = U.splat; import './Chart.js'; import './Pointer.js'; import './Series.js'; import './SvgRenderer.js'; // Has a dependency on Navigator due to the use of // defaultOptions.navigator import './Navigator.js'; // Has a dependency on Scrollbar due to the use of // defaultOptions.scrollbar import './Scrollbar.js'; // Has a dependency on RangeSelector due to the use of // defaultOptions.rangeSelector import './RangeSelector.js'; var Chart = H.Chart, Renderer = H.Renderer, Series = H.Series, SVGRenderer = H.SVGRenderer, seriesProto = Series.prototype, seriesInit = seriesProto.init, seriesProcessData = seriesProto.processData, pointTooltipFormatter = Point.prototype.tooltipFormatter; /** * Compare the values of the series against the first non-null, non- * zero value in the visible range. The y axis will show percentage * or absolute change depending on whether `compare` is set to `"percent"` * or `"value"`. When this is applied to multiple series, it allows * comparing the development of the series against each other. Adds * a `change` field to every point object. * * @see [compareBase](#plotOptions.series.compareBase) * @see [Axis.setCompare()](/class-reference/Highcharts.Axis#setCompare) * * @sample {highstock} stock/plotoptions/series-compare-percent/ * Percent * @sample {highstock} stock/plotoptions/series-compare-value/ * Value * * @type {string} * @since 1.0.1 * @product highstock * @apioption plotOptions.series.compare */ /** * Defines if comparison should start from the first point within the visible * range or should start from the first point **before** the range. * * In other words, this flag determines if first point within the visible range * will have 0% (`compareStart=true`) or should have been already calculated * according to the previous point (`compareStart=false`). * * @sample {highstock} stock/plotoptions/series-comparestart/ * Calculate compare within visible range * * @type {boolean} * @default false * @since 6.0.0 * @product highstock * @apioption plotOptions.series.compareStart */ /** * When [compare](#plotOptions.series.compare) is `percent`, this option * dictates whether to use 0 or 100 as the base of comparison. * * @sample {highstock} stock/plotoptions/series-comparebase/ * Compare base is 100 * * @type {number} * @default 0 * @since 5.0.6 * @product highstock * @validvalue [0, 100] * @apioption plotOptions.series.compareBase */ /* eslint-disable no-invalid-this, valid-jsdoc */ /** * Factory function for creating new stock charts. Creates a new * {@link Highcharts.Chart|Chart} object with different default options than the * basic Chart. * * @example * var chart = Highcharts.stockChart('container', { * series: [{ * data: [1, 2, 3, 4, 5, 6, 7, 8, 9], * pointInterval: 24 * 60 * 60 * 1000 * }] * }); * * @function Highcharts.stockChart * * @param {string|Highcharts.HTMLDOMElement} [renderTo] * The DOM element to render to, or its id. * * @param {Highcharts.Options} options * The chart options structure as described in the * [options reference](https://api.highcharts.com/highstock). * * @param {Highcharts.ChartCallbackFunction} [callback] * A function to execute when the chart object is finished loading and * rendering. In most cases the chart is built in one thread, but in * Internet Explorer version 8 or less the chart is sometimes * initialized before the document is ready, and in these cases the * chart object will not be finished synchronously. As a consequence, * code that relies on the newly built Chart object should always run in * the callback. Defining a * [chart.events.load](https://api.highcharts.com/highstock/chart.events.load) * handler is equivalent. * * @return {Highcharts.Chart} * The chart object. */ H.StockChart = H.stockChart = function (a, b, c) { var hasRenderToArg = isString(a) || a.nodeName, options = arguments[hasRenderToArg ? 1 : 0], userOptions = options, // to increase performance, don't merge the data seriesOptions = options.series, defaultOptions = H.getOptions(), opposite, // Always disable startOnTick:true on the main axis when the navigator // is enabled (#1090) navigatorEnabled = pick(options.navigator && options.navigator.enabled, defaultOptions.navigator.enabled, true); // apply X axis options to both single and multi y axes options.xAxis = splat(options.xAxis || {}).map(function (xAxisOptions, i) { return merge({ minPadding: 0, maxPadding: 0, overscroll: 0, ordinal: true, title: { text: null }, labels: { overflow: 'justify' }, showLastLabel: true }, defaultOptions.xAxis, // #3802 defaultOptions.xAxis && defaultOptions.xAxis[i], // #7690 xAxisOptions, // user options { type: 'datetime', categories: null }, (navigatorEnabled ? { startOnTick: false, endOnTick: false } : null)); }); // apply Y axis options to both single and multi y axes options.yAxis = splat(options.yAxis || {}).map(function (yAxisOptions, i) { opposite = pick(yAxisOptions.opposite, true); return merge({ labels: { y: -2 }, opposite: opposite, /** * @default {highcharts} true * @default {highstock} false * @apioption yAxis.showLastLabel * * @private */ showLastLabel: !!( // #6104, show last label by default for category axes yAxisOptions.categories || yAxisOptions.type === 'category'), title: { text: null } }, defaultOptions.yAxis, // #3802 defaultOptions.yAxis && defaultOptions.yAxis[i], // #7690 yAxisOptions // user options ); }); options.series = null; options = merge({ chart: { panning: { enabled: true, type: 'x' }, pinchType: 'x' }, navigator: { enabled: navigatorEnabled }, scrollbar: { // #4988 - check if setOptions was called enabled: pick(defaultOptions.scrollbar.enabled, true) }, rangeSelector: { // #4988 - check if setOptions was called enabled: pick(defaultOptions.rangeSelector.enabled, true) }, title: { text: null }, tooltip: { split: pick(defaultOptions.tooltip.split, true), crosshairs: true }, legend: { enabled: false } }, options, // user's options { isStock: true // internal flag }); options.series = userOptions.series = seriesOptions; return hasRenderToArg ? new Chart(a, options, c) : new Chart(options, b); }; // Handle som Stock-specific series defaults, override the plotOptions before // series options are handled. addEvent(Series, 'setOptions', function (e) { var overrides; if (this.chart.options.isStock) { if (this.is('column') || this.is('columnrange')) { overrides = { borderWidth: 0, shadow: false }; } else if (!this.is('scatter') && !this.is('sma')) { overrides = { marker: { enabled: false, radius: 2 } }; } if (overrides) { e.plotOptions[this.type] = merge(e.plotOptions[this.type], overrides); } } }); // Override the automatic label alignment so that the first Y axis' labels // are drawn on top of the grid line, and subsequent axes are drawn outside addEvent(Axis, 'autoLabelAlign', function (e) { var chart = this.chart, options = this.options, panes = chart._labelPanes = chart._labelPanes || {}, key, labelOptions = this.options.labels; if (this.chart.options.isStock && this.coll === 'yAxis') { key = options.top + ',' + options.height; // do it only for the first Y axis of each pane if (!panes[key] && labelOptions.enabled) { if (labelOptions.x === 15) { // default labelOptions.x = 0; } if (typeof labelOptions.align === 'undefined') { labelOptions.align = 'right'; } panes[key] = this; e.align = 'right'; e.preventDefault(); } } }); // Clear axis from label panes (#6071) addEvent(Axis, 'destroy', function () { var chart = this.chart, key = this.options && (this.options.top + ',' + this.options.height); if (key && chart._labelPanes && chart._labelPanes[key] === this) { delete chart._labelPanes[key]; } }); // Override getPlotLinePath to allow for multipane charts addEvent(Axis, 'getPlotLinePath', function (e) { var axis = this, series = (this.isLinked && !this.series ? this.linkedParent.series : this.series), chart = axis.chart, renderer = chart.renderer, axisLeft = axis.left, axisTop = axis.top, x1, y1, x2, y2, result = [], axes = [], // #3416 need a default array axes2, uniqueAxes, translatedValue = e.translatedValue, value = e.value, force = e.force, transVal; /** * Return the other axis based on either the axis option or on related * series. * @private */ function getAxis(coll) { var otherColl = coll === 'xAxis' ? 'yAxis' : 'xAxis', opt = axis.options[otherColl]; // Other axis indexed by number if (isNumber(opt)) { return [chart[otherColl][opt]]; } // Other axis indexed by id (like navigator) if (isString(opt)) { return [chart.get(opt)]; } // Auto detect based on existing series return series.map(function (s) { return s[otherColl]; }); } if ( // For stock chart, by default render paths across the panes // except the case when `acrossPanes` is disabled by user (#6644) (chart.options.isStock && e.acrossPanes !== false) && // Ignore in case of colorAxis or zAxis. #3360, #3524, #6720 axis.coll === 'xAxis' || axis.coll === 'yAxis') { e.preventDefault(); // Get the related axes based on series axes = getAxis(axis.coll); // Get the related axes based options.*Axis setting #2810 axes2 = (axis.isXAxis ? chart.yAxis : chart.xAxis); axes2.forEach(function (A) { if (defined(A.options.id) ? A.options.id.indexOf('navigator') === -1 : true) { var a = (A.isXAxis ? 'yAxis' : 'xAxis'), rax = (defined(A.options[a]) ? chart[a][A.options[a]] : chart[a][0]); if (axis === rax) { axes.push(A); } } }); // Remove duplicates in the axes array. If there are no axes in the axes // array, we are adding an axis without data, so we need to populate // this with grid lines (#2796). uniqueAxes = axes.length ? [] : [axis.isXAxis ? chart.yAxis[0] : chart.xAxis[0]]; // #3742 axes.forEach(function (axis2) { if (uniqueAxes.indexOf(axis2) === -1 && // Do not draw on axis which overlap completely. #5424 !find(uniqueAxes, function (unique) { return unique.pos === axis2.pos && unique.len === axis2.len; })) { uniqueAxes.push(axis2); } }); transVal = pick(translatedValue, axis.translate(value, null, null, e.old)); if (isNumber(transVal)) { if (axis.horiz) { uniqueAxes.forEach(function (axis2) { var skip; y1 = axis2.pos; y2 = y1 + axis2.len; x1 = x2 = Math.round(transVal + axis.transB); // outside plot area if (force !== 'pass' && (x1 < axisLeft || x1 > axisLeft + axis.width)) { if (force) { x1 = x2 = clamp(x1, axisLeft, axisLeft + axis.width); } else { skip = true; } } if (!skip) { result.push(['M', x1, y1], ['L', x2, y2]); } }); } else { uniqueAxes.forEach(function (axis2) { var skip; x1 = axis2.pos; x2 = x1 + axis2.len; y1 = y2 = Math.round(axisTop + axis.height - transVal); // outside plot area if (force !== 'pass' && (y1 < axisTop || y1 > axisTop + axis.height)) { if (force) { y1 = y2 = clamp(y1, axisTop, axisTop + axis.height); } else { skip = true; } } if (!skip) { result.push(['M', x1, y1], ['L', x2, y2]); } }); } } e.path = result.length > 0 ? renderer.crispPolyLine(result, e.lineWidth || 1) : // #3557 getPlotLinePath in regular Highcharts also returns null null; } }); /** * Function to crisp a line with multiple segments * * @private * @function Highcharts.SVGRenderer#crispPolyLine * @param {Highcharts.SVGPathArray} points * @param {number} width * @return {Highcharts.SVGPathArray} */ SVGRenderer.prototype.crispPolyLine = function (points, width) { // points format: [['M', 0, 0], ['L', 100, 0]] // normalize to a crisp line for (var i = 0; i < points.length; i = i + 2) { var start = points[i], end = points[i + 1]; if (start[1] === end[1]) { // Substract due to #1129. Now bottom and left axis gridlines behave // the same. start[1] = end[1] = Math.round(start[1]) - (width % 2 / 2); } if (start[2] === end[2]) { start[2] = end[2] = Math.round(start[2]) + (width % 2 / 2); } } return points; }; // Wrapper to hide the label addEvent(Axis, 'afterHideCrosshair', function () { if (this.crossLabel) { this.crossLabel = this.crossLabel.hide(); } }); // Extend crosshairs to also draw the label addEvent(Axis, 'afterDrawCrosshair', function (event) { // Check if the label has to be drawn if (!defined(this.crosshair.label) || !this.crosshair.label.enabled || !this.cross) { return; } var chart = this.chart, log = this.logarithmic, options = this.options.crosshair.label, // the label's options horiz = this.horiz, // axis orientation opposite = this.opposite, // axis position left = this.left, // left position top = this.top, // top position crossLabel = this.crossLabel, // the svgElement posx, posy, crossBox, formatOption = options.format, formatFormat = '', limit, align, tickInside = this.options.tickPosition === 'inside', snap = this.crosshair.snap !== false, value, offset = 0, // Use last available event (#5287) e = event.e || (this.cross && this.cross.e), point = event.point, min = this.min, max = this.max; if (log) { min = log.lin2log(min); max = log.lin2log(max); } align = (horiz ? 'center' : opposite ? (this.labelAlign === 'right' ? 'right' : 'left') : (this.labelAlign === 'left' ? 'left' : 'center')); // If the label does not exist yet, create it. if (!crossLabel) { crossLabel = this.crossLabel = chart.renderer .label(null, null, null, options.shape || 'callout') .addClass('highcharts-crosshair-label' + (this.series[0] && ' highcharts-color-' + this.series[0].colorIndex)) .attr({ align: options.align || align, padding: pick(options.padding, 8), r: pick(options.borderRadius, 3), zIndex: 2 }) .add(this.labelGroup); // Presentational if (!chart.styledMode) { crossLabel .attr({ fill: options.backgroundColor || (this.series[0] && this.series[0].color) || '#666666', stroke: options.borderColor || '', 'stroke-width': options.borderWidth || 0 }) .css(extend({ color: '#ffffff', fontWeight: 'normal', fontSize: '11px', textAlign: 'center' }, options.style)); } } if (horiz) { posx = snap ? point.plotX + left : e.chartX; posy = top + (opposite ? 0 : this.height); } else { posx = opposite ? this.width + left : 0; posy = snap ? point.plotY + top : e.chartY; } if (!formatOption && !options.formatter) { if (this.dateTime) { formatFormat = '%b %d, %Y'; } formatOption = '{value' + (formatFormat ? ':' + formatFormat : '') + '}'; } // Show the label value = snap ? point[this.isXAxis ? 'x' : 'y'] : this.toValue(horiz ? e.chartX : e.chartY); crossLabel.attr({ text: formatOption ? format(formatOption, { value: value }, chart) : options.formatter.call(this, value), x: posx, y: posy, // Crosshair should be rendered within Axis range (#7219) visibility: value < min || value > max ? 'hidden' : 'visible' }); crossBox = crossLabel.getBBox(); // now it is placed we can correct its position if (horiz) { if ((tickInside && !opposite) || (!tickInside && opposite)) { posy = crossLabel.y - crossBox.height; } } else { posy = crossLabel.y - (crossBox.height / 2); } // check the edges if (horiz) { limit = { left: left - crossBox.x, right: left + this.width - crossBox.x }; } else { limit = { left: this.labelAlign === 'left' ? left : 0, right: this.labelAlign === 'right' ? left + this.width : chart.chartWidth }; } // left edge if (crossLabel.translateX < limit.left) { offset = limit.left - crossLabel.translateX; } // right edge if (crossLabel.translateX + crossBox.width >= limit.right) { offset = -(crossLabel.translateX + crossBox.width - limit.right); } // show the crosslabel crossLabel.attr({ x: posx + offset, y: posy, // First set x and y, then anchorX and anchorY, when box is actually // calculated, #5702 anchorX: horiz ? posx : (this.opposite ? 0 : chart.chartWidth), anchorY: horiz ? (this.opposite ? chart.chartHeight : 0) : posy + crossBox.height / 2 }); }); /* ************************************************************************** * * Start value compare logic * * ************************************************************************** */ /** * Extend series.init by adding a method to modify the y value used for plotting * on the y axis. This method is called both from the axis when finding dataMin * and dataMax, and from the series.translate method. * * @ignore * @function Highcharts.Series#init */ seriesProto.init = function () { // Call base method seriesInit.apply(this, arguments); // Set comparison mode this.setCompare(this.options.compare); }; /** * Highstock only. Set the * [compare](https://api.highcharts.com/highstock/plotOptions.series.compare) * mode of the series after render time. In most cases it is more useful running * {@link Axis#setCompare} on the X axis to update all its series. * * @function Highcharts.Series#setCompare * * @param {string} [compare] * Can be one of `null` (default), `"percent"` or `"value"`. */ seriesProto.setCompare = function (compare) { // Set or unset the modifyValue method this.modifyValue = (compare === 'value' || compare === 'percent') ? function (value, point) { var compareValue = this.compareValue; if (typeof value !== 'undefined' && typeof compareValue !== 'undefined') { // #2601, #5814 // Get the modified value if (compare === 'value') { value -= compareValue; // Compare percent } else { value = 100 * (value / compareValue) - (this.options.compareBase === 100 ? 0 : 100); } // record for tooltip etc. if (point) { point.change = value; } return value; } return 0; } : null; // Survive to export, #5485 this.userOptions.compare = compare; // Mark dirty if (this.chart.hasRendered) { this.isDirty = true; } }; /** * Extend series.processData by finding the first y value in the plot area, * used for comparing the following values * * @ignore * @function Highcharts.Series#processData */ seriesProto.processData = function (force) { var series = this, i, keyIndex = -1, processedXData, processedYData, compareStart = series.options.compareStart === true ? 0 : 1, length, compareValue; // call base method seriesProcessData.apply(this, arguments); if (series.xAxis && series.processedYData) { // not pies // local variables processedXData = series.processedXData; processedYData = series.processedYData; length = processedYData.length; // For series with more than one value (range, OHLC etc), compare // against close or the pointValKey (#4922, #3112, #9854) if (series.pointArrayMap) { keyIndex = series.pointArrayMap.indexOf(series.options.pointValKey || series.pointValKey || 'y'); } // find the first value for comparison for (i = 0; i < length - compareStart; i++) { compareValue = processedYData[i] && keyIndex > -1 ? processedYData[i][keyIndex] : processedYData[i]; if (isNumber(compareValue) && processedXData[i + compareStart] >= series.xAxis.min && compareValue !== 0) { series.compareValue = compareValue; break; } } } return; }; // Modify series extremes addEvent(Series, 'afterGetExtremes', function (e) { var dataExtremes = e.dataExtremes; if (this.modifyValue && dataExtremes) { var extremes = [ this.modifyValue(dataExtremes.dataMin), this.modifyValue(dataExtremes.dataMax) ]; dataExtremes.dataMin = arrayMin(extremes); dataExtremes.dataMax = arrayMax(extremes); } }); /** * Highstock only. Set the compare mode on all series belonging to an Y axis * after render time. * * @see [series.plotOptions.compare](https://api.highcharts.com/highstock/series.plotOptions.compare) * * @sample stock/members/axis-setcompare/ * Set compoare * * @function Highcharts.Axis#setCompare * * @param {string} [compare] * The compare mode. Can be one of `null` (default), `"value"` or * `"percent"`. * * @param {boolean} [redraw=true] * Whether to redraw the chart or to wait for a later call to * {@link Chart#redraw}. */ Axis.prototype.setCompare = function (compare, redraw) { if (!this.isXAxis) { this.series.forEach(function (series) { series.setCompare(compare); }); if (pick(redraw, true)) { this.chart.redraw(); } } }; /** * Extend the tooltip formatter by adding support for the point.change variable * as well as the changeDecimals option. * * @ignore * @function Highcharts.Point#tooltipFormatter * * @param {string} pointFormat */ Point.prototype.tooltipFormatter = function (pointFormat) { var point = this; var numberFormatter = point.series.chart.numberFormatter; pointFormat = pointFormat.replace('{point.change}', (point.change > 0 ? '+' : '') + numberFormatter(point.change, pick(point.series.tooltipOptions.changeDecimals, 2))); return pointTooltipFormatter.apply(this, [pointFormat]); }; /* ************************************************************************** * * End value compare logic * * ************************************************************************** */ // Extend the Series prototype to create a separate series clip box. This is // related to using multiple panes, and a future pane logic should incorporate // this feature (#2754). addEvent(Series, 'render', function () { var chart = this.chart, clipHeight; // Only do this on not 3d (#2939, #5904) nor polar (#6057) charts, and only // if the series type handles clipping in the animate method (#2975). if (!(chart.is3d && chart.is3d()) && !chart.polar && this.xAxis && !this.xAxis.isRadial // Gauge, #6192 ) { clipHeight = this.yAxis.len; // Include xAxis line width (#8031) but only if the Y axis ends on the // edge of the X axis (#11005). if (this.xAxis.axisLine) { var dist = chart.plotTop + chart.plotHeight - this.yAxis.pos - this.yAxis.len, lineHeightCorrection = Math.floor(this.xAxis.axisLine.strokeWidth() / 2); if (dist >= 0) { clipHeight -= Math.max(lineHeightCorrection - dist, 0); } } // First render, initial clip box if (!this.clipBox && this.animate) { this.clipBox = merge(chart.clipBox); this.clipBox.width = this.xAxis.len; this.clipBox.height = clipHeight; // On redrawing, resizing etc, update the clip rectangle } else if (chart[this.sharedClipKey]) { // animate in case resize is done during initial animation chart[this.sharedClipKey].animate({ width: this.xAxis.len, height: clipHeight }); // also change markers clip animation for consistency // (marker clip rects should exist only on chart init) if (chart[this.sharedClipKey + 'm']) { chart[this.sharedClipKey + 'm'].animate({ width: this.xAxis.len }); } } } }); addEvent(Chart, 'update', function (e) { var options = e.options; // Use case: enabling scrollbar from a disabled state. // Scrollbar needs to be initialized from a controller, Navigator in this // case (#6615) if ('scrollbar' in options && this.navigator) { merge(true, this.options.scrollbar, options.scrollbar); this.navigator.update({}, false); delete options.scrollbar; } });