app/assets/javascripts/highcharts/modules/stock.js in highcharts-rails-5.0.14 vs app/assets/javascripts/highcharts/modules/stock.js in highcharts-rails-6.0.0

- old
+ new

@@ -1,7 +1,7 @@ /** - * @license Highcharts JS v5.0.14 (2017-07-28) + * @license Highcharts JS v6.0.0 (2017-10-04) * Highstock as a plugin for Highcharts * * (c) 2017 Torstein Honsi * * License: www.highcharts.com/license @@ -27,10 +27,11 @@ dateFormat = H.dateFormat, defined = H.defined, each = H.each, extend = H.extend, noop = H.noop, + pick = H.pick, Series = H.Series, timeUnits = H.timeUnits, wrap = H.wrap; /* **************************************************************************** @@ -236,29 +237,65 @@ minIndex, maxIndex, slope, hasBreaks = axis.isXAxis && !!axis.options.breaks, isOrdinal = axis.options.ordinal, + overscrollPointsRange = Number.MAX_SAFE_INTEGER, ignoreHiddenSeries = axis.chart.options.chart.ignoreHiddenSeries, + isNavigatorAxis = axis.options.className === 'highcharts-navigator-xaxis', i; - // apply the ordinal logic + if ( + axis.options.overscroll && + axis.max === axis.dataMax && + ( + // Panning is an execption, + // We don't want to apply overscroll when panning over the dataMax + !axis.chart.mouseIsDown || + isNavigatorAxis + ) && ( + // Scrollbar buttons are the other execption: + !axis.eventArgs || + axis.eventArgs && axis.eventArgs.trigger !== 'navigator' + ) + ) { + axis.max += axis.options.overscroll; + + // Live data and buttons require translation for the min: + if (!isNavigatorAxis && defined(axis.userMin)) { + axis.min += axis.options.overscroll; + } + } + + // Apply the ordinal logic if (isOrdinal || hasBreaks) { // #4167 YAxis is never ordinal ? each(axis.series, function(series, i) { - if ((!ignoreHiddenSeries || series.visible !== false) && (series.takeOrdinalPosition !== false || hasBreaks)) { + if ( + (!ignoreHiddenSeries || series.visible !== false) && + (series.takeOrdinalPosition !== false || hasBreaks) + ) { // concatenate the processed X data into the existing positions, or the empty array ordinalPositions = ordinalPositions.concat(series.processedXData); len = ordinalPositions.length; // remove duplicates (#1588) ordinalPositions.sort(function(a, b) { return a - b; // without a custom function it is sorted as strings }); + overscrollPointsRange = Math.min( + overscrollPointsRange, + pick( + // Check for a single-point series: + series.closestPointRange, + overscrollPointsRange + ) + ); + if (len) { i = len - 1; while (i--) { if (ordinalPositions[i] === ordinalPositions[i + 1]) { ordinalPositions.splice(i, 1); @@ -283,20 +320,43 @@ } } // When zooming in on a week, prevent axis padding for weekends even though the data within // the week is evenly spaced. - if (!axis.options.keepOrdinalPadding && (ordinalPositions[0] - min > dist || max - ordinalPositions[ordinalPositions.length - 1] > dist)) { + if (!axis.options.keepOrdinalPadding && + ( + ordinalPositions[0] - min > dist || + max - ordinalPositions[ordinalPositions.length - 1] > dist + ) + ) { useOrdinal = true; } + } else if (axis.options.overscroll) { + if (len === 2) { + // Exactly two points, distance for overscroll is fixed: + overscrollPointsRange = ordinalPositions[1] - ordinalPositions[0]; + } else if (len === 1) { + // We have just one point, closest distance is unknown. + // Assume then it is last point and overscrolled range: + overscrollPointsRange = axis.options.overscroll; + ordinalPositions = [ordinalPositions[0], ordinalPositions[0] + overscrollPointsRange]; + } else { + // In case of zooming in on overscrolled range, stick to the old range: + overscrollPointsRange = axis.overscrollPointsRange; + } } // Record the slope and offset to compute the linear values from the array index. // Since the ordinal positions may exceed the current range, get the start and // end positions within it (#719, #665b) if (useOrdinal) { + if (axis.options.overscroll) { + axis.overscrollPointsRange = overscrollPointsRange; + ordinalPositions = ordinalPositions.concat(axis.getOverscrollPositions()); + } + // Register axis.ordinalPositions = ordinalPositions; // This relies on the ordinalPositions being set. Use Math.max // and Math.min to prevent padding on either sides of the data. @@ -318,10 +378,11 @@ // Set the slope and offset of the values compared to the indices in the ordinal positions axis.ordinalSlope = slope = (max - min) / (maxIndex - minIndex); axis.ordinalOffset = min - (minIndex * slope); } else { + axis.overscrollPointsRange = pick(axis.closestPointRange, axis.overscrollPointsRange); axis.ordinalPositions = axis.ordinalSlope = axis.ordinalOffset = undefined; } } axis.isOrdinal = isOrdinal && useOrdinal; // #3818, #4196, #4926 axis.groupIntervalFactor = null; // reset for next run @@ -442,10 +503,11 @@ var axis = this, chart = axis.chart, grouping = axis.series[0].currentDataGrouping, ordinalIndex = axis.ordinalIndex, key = grouping ? grouping.count + grouping.unitName : 'raw', + overscroll = axis.options.overscroll, extremes = axis.getExtremes(), fakeAxis, fakeSeries; // If this is the first time, or the ordinal index is deleted by updatedData, @@ -462,11 +524,11 @@ series: [], chart: chart, getExtremes: function() { return { min: extremes.dataMin, - max: extremes.dataMax + max: extremes.dataMax + overscroll }; }, options: { ordinal: true }, @@ -476,14 +538,17 @@ // Add the fake series to hold the full data, then apply processData to it each(axis.series, function(series) { fakeSeries = { xAxis: fakeAxis, - xData: series.xData, + xData: series.xData.slice(), chart: chart, destroyGroupedData: noop }; + + fakeSeries.xData = fakeSeries.xData.concat(axis.getOverscrollPositions()); + fakeSeries.options = { dataGrouping: grouping ? { enabled: true, forced: true, approximation: 'open', // doesn't matter which, use the fastest @@ -494,10 +559,11 @@ enabled: false } }; series.processData.apply(fakeSeries); + fakeAxis.series.push(fakeSeries); }); // Run beforeSetTickPositions to compute the ordinalPositions axis.beforeSetTickPositions.apply(fakeAxis); @@ -507,10 +573,42 @@ } return ordinalIndex[key]; }, /** + * Get ticks for an ordinal axis within a range where points don't exist. + * It is required when overscroll is enabled. We can't base on points, + * because we may not have any, so we use approximated pointRange and + * generate these ticks between <Axis.dataMax, Axis.dataMax + Axis.overscroll> + * evenly spaced. Used in panning and navigator scrolling. + * + * @returns positions {Array} Generated ticks + * @private + */ + getOverscrollPositions: function() { + var axis = this, + extraRange = axis.options.overscroll, + distance = axis.overscrollPointsRange, + positions = [], + max = axis.dataMax; + + if (H.defined(distance)) { + // Max + pointRange because we need to scroll to the last + + positions.push(max); + + while (max <= axis.dataMax + extraRange) { + max += distance; + positions.push(max); + } + + } + + return positions; + }, + + /** * Find the factor to estimate how wide the plot area would have been if ordinal * gaps were included. This value is used to compute an imagined plot width in order * to establish the data grouping interval. * * A real world case is the intraday-candlestick @@ -593,10 +691,11 @@ // Extending the Chart.pan method for ordinal axes wrap(Chart.prototype, 'pan', function(proceed, e) { var chart = this, xAxis = chart.xAxis[0], + overscroll = xAxis.options.overscroll, chartX = e.chartX, runBase = false; if (xAxis.options.ordinal && xAxis.series.length) { @@ -605,11 +704,11 @@ dataMax = extremes.dataMax, min = extremes.min, max = extremes.max, trimmedRange, hoverPoints = chart.hoverPoints, - closestPointRange = xAxis.closestPointRange, + closestPointRange = xAxis.closestPointRange || xAxis.overscrollPointsRange, pointPixelWidth = xAxis.translationSlope * (xAxis.ordinalSlope || closestPointRange), movedUnits = (mouseDownX - chartX) / pointPixelWidth, // how many ordinal units did we move? extendedAxis = { ordinalPositions: xAxis.getExtendedPositions() }, // get index of all the chart's points @@ -662,11 +761,14 @@ true // translate from index ]) ); // Apply it if it is within the available data range - if (trimmedRange.min >= Math.min(extremes.dataMin, min) && trimmedRange.max <= Math.max(dataMax, max)) { + if ( + trimmedRange.min >= Math.min(extremes.dataMin, min) && + trimmedRange.max <= Math.max(dataMax, max) + overscroll + ) { xAxis.setExtremes(trimmedRange.min, trimmedRange.max, true, false, { trigger: 'pan' }); } @@ -680,10 +782,13 @@ runBase = true; } // revert to the linear chart.pan version if (runBase) { + if (overscroll) { + xAxis.max = xAxis.dataMax + overscroll; + } // call the original function proceed.apply(this, Array.prototype.slice.call(arguments, 1)); } }); @@ -1061,11 +1166,11 @@ * graph. * * @type {String} * @see [gapSize](plotOptions.series.gapSize) * @default relative - * @validvalues ["relative", "value"] + * @validvalue ["relative", "value"] * @since 5.0.13 * @product highstock * @apioption plotOptions.series.gapUnit */ @@ -1140,22 +1245,207 @@ /* **************************************************************************** * Start data grouping module * ******************************************************************************/ + /** + * Data grouping is the concept of sampling the data values into larger + * blocks in order to ease readability and increase performance of the + * JavaScript charts. Highstock by default applies data grouping when + * the points become closer than a certain pixel value, determined by + * the `groupPixelWidth` option. + * + * If data grouping is applied, the grouping information of grouped + * points can be read from the [Point.dataGroup](#Point.dataGroup). + * + * @product highstock + * @apioption plotOptions.series.dataGrouping + */ + + /** + * The method of approximation inside a group. When for example 30 days + * are grouped into one month, this determines what value should represent + * the group. Possible values are "average", "averages", "open", "high", + * "low", "close" and "sum". For OHLC and candlestick series the approximation + * is "ohlc" by default, which finds the open, high, low and close values + * within all the grouped data. For ranges, the approximation is "range", + * which finds the low and high values. For multi-dimensional data, + * like ranges and OHLC, "averages" will compute the average for each + * dimension. + * + * Custom aggregate methods can be added by assigning a callback function + * as the approximation. This function takes a numeric array as the + * argument and should return a single numeric value or `null`. Note + * that the numeric array will never contain null values, only true + * numbers. Instead, if null values are present in the raw data, the + * numeric array will have an `.hasNulls` property set to `true`. For + * single-value data sets the data is available in the first argument + * of the callback function. For OHLC data sets, all the open values + * are in the first argument, all high values in the second etc. + * + * Since v4.2.7, grouping meta data is available in the approximation + * callback from `this.dataGroupInfo`. It can be used to extract information + * from the raw data. + * + * Defaults to `average` for line-type series, `sum` for columns, `range` + * for range series and `ohlc` for OHLC and candlestick. + * + * @validvalue ["average", "averages", "open", "high", "low", "close", "sum"] + * @type {String|Function} + * @sample {highstock} stock/plotoptions/series-datagrouping-approximation Approximation callback with custom data + * @product highstock + * @apioption plotOptions.series.dataGrouping.approximation + */ + + /** + * Datetime formats for the header of the tooltip in a stock chart. + * The format can vary within a chart depending on the currently selected + * time range and the current data grouping. + * + * The default formats are: + * + * <pre>{ + * millisecond: ['%A, %b %e, %H:%M:%S.%L', '%A, %b %e, %H:%M:%S.%L', '-%H:%M:%S.%L'], + * second: ['%A, %b %e, %H:%M:%S', '%A, %b %e, %H:%M:%S', '-%H:%M:%S'], + * minute: ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'], + * hour: ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'], + * day: ['%A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'], + * week: ['Week from %A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'], + * month: ['%B %Y', '%B', '-%B %Y'], + * year: ['%Y', '%Y', '-%Y'] + * }</pre> + * + * For each of these array definitions, the first item is the format + * used when the active time span is one unit. For instance, if the + * current data applies to one week, the first item of the week array + * is used. The second and third items are used when the active time + * span is more than two units. For instance, if the current data applies + * to two weeks, the second and third item of the week array are used, + * and applied to the start and end date of the time span. + * + * @type {Object} + * @product highstock + * @apioption plotOptions.series.dataGrouping.dateTimeLabelFormats + */ + + /** + * Enable or disable data grouping. + * + * @type {Boolean} + * @default true + * @product highstock + * @apioption plotOptions.series.dataGrouping.enabled + */ + + /** + * When data grouping is forced, it runs no matter how small the intervals + * are. This can be handy for example when the sum should be calculated + * for values appearing at random times within each hour. + * + * @type {Boolean} + * @default false + * @product highstock + * @apioption plotOptions.series.dataGrouping.forced + */ + + /** + * The approximate pixel width of each group. If for example a series + * with 30 points is displayed over a 600 pixel wide plot area, no grouping + * is performed. If however the series contains so many points that + * the spacing is less than the groupPixelWidth, Highcharts will try + * to group it into appropriate groups so that each is more or less + * two pixels wide. If multiple series with different group pixel widths + * are drawn on the same x axis, all series will take the greatest width. + * For example, line series have 2px default group width, while column + * series have 10px. If combined, both the line and the column will + * have 10px by default. + * + * @type {Number} + * @default 2 + * @product highstock + * @apioption plotOptions.series.dataGrouping.groupPixelWidth + */ + + /** + * Normally, a group is indexed by the start of that group, so for example + * when 30 daily values are grouped into one month, that month's x value + * will be the 1st of the month. This apparently shifts the data to + * the left. When the smoothed option is true, this is compensated for. + * The data is shifted to the middle of the group, and min and max + * values are preserved. Internally, this is used in the Navigator series. + * + * @type {Boolean} + * @default false + * @product highstock + * @apioption plotOptions.series.dataGrouping.smoothed + */ + + /** + * An array determining what time intervals the data is allowed to be + * grouped to. Each array item is an array where the first value is + * the time unit and the second value another array of allowed multiples. + * Defaults to: + * + * <pre>units: [[ + * 'millisecond', // unit name + * [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples + * ], [ + * 'second', + * [1, 2, 5, 10, 15, 30] + * ], [ + * 'minute', + * [1, 2, 5, 10, 15, 30] + * ], [ + * 'hour', + * [1, 2, 3, 4, 6, 8, 12] + * ], [ + * 'day', + * [1] + * ], [ + * 'week', + * [1] + * ], [ + * 'month', + * [1, 3, 6] + * ], [ + * 'year', + * null + * ]]</pre> + * + * @type {Array} + * @product highstock + * @apioption plotOptions.series.dataGrouping.units + */ + + /** + * The approximate pixel width of each group. If for example a series + * with 30 points is displayed over a 600 pixel wide plot area, no grouping + * is performed. If however the series contains so many points that + * the spacing is less than the groupPixelWidth, Highcharts will try + * to group it into appropriate groups so that each is more or less + * two pixels wide. Defaults to `10`. + * + * @type {Number} + * @sample {highstock} stock/plotoptions/series-datagrouping-grouppixelwidth/ + * Two series with the same data density but different groupPixelWidth + * @default 10 + * @product highstock + * @apioption plotOptions.column.dataGrouping.groupPixelWidth + */ + var seriesProto = Series.prototype, baseProcessData = seriesProto.processData, baseGeneratePoints = seriesProto.generatePoints, baseDestroy = seriesProto.destroy, /** * */ commonOptions = { approximation: 'average', // average, open, high, low, close, sum - //enabled: null, // (true for stock charts, false for basic), - //forced: undefined, + // enabled: null, // (true for stock charts, false for basic), + // forced: undefined, groupPixelWidth: 2, // the first one is the point or start value, the second is the start value if we're dealing with range, // the third one is the end value if dealing with a range dateTimeLabelFormats: { millisecond: ['%A, %b %e, %H:%M:%S.%L', '%A, %b %e, %H:%M:%S.%L', '-%H:%M:%S.%L'], @@ -1235,11 +1525,11 @@ * approximations takes an array or numbers as the first parameter. In case * of ohlc, four arrays are sent in as four parameters. Each array consists * only of numbers. In case null values belong to the group, the property * .hasNulls will be set to true on the array. */ - approximations = { + approximations = H.approximations = { sum: function(arr) { var len = arr.length, ret; // 1. it consists of nulls exclusively @@ -1315,11 +1605,10 @@ } // else, return is undefined } }; - /** * Takes parallel arrays of x and y data and groups the data into intervals * defined by groupPositions, a collection of starting x values for each group. */ seriesProto.groupData = function(xData, yData, groupPositions, approximation) { @@ -1506,11 +1795,11 @@ groupedXData = groupedData[0], groupedYData = groupedData[1]; // prevent the smoothed data to spill out left and right, and make // sure data is not shifted to the left - if (dataGroupingOptions.smoothed) { + if (dataGroupingOptions.smoothed && groupedXData.length) { i = groupedXData.length - 1; groupedXData[i] = Math.min(groupedXData[i], xMax); while (i-- && i > 0) { groupedXData[i] += interval / 2; } @@ -1812,76 +2101,96 @@ * * @constructor seriesTypes.ohlc * @augments seriesTypes.column */ /** + * An OHLC chart is a style of financial chart used to describe price + * movements over time. It displays open, high, low and close values per data + * point. + * + * @sample stock/demo/ohlc/ OHLC chart * @extends {plotOptions.column} + * @excluding borderColor,borderRadius,borderWidth + * @product highstock * @optionparent plotOptions.ohlc */ seriesType('ohlc', 'column', { /** + * The approximate pixel width of each group. If for example a series + * with 30 points is displayed over a 600 pixel wide plot area, no grouping + * is performed. If however the series contains so many points that + * the spacing is less than the groupPixelWidth, Highcharts will try + * to group it into appropriate groups so that each is more or less + * two pixels wide. Defaults to `5`. + * + * @type {Number} + * @default 5 + * @product highstock + * @apioption plotOptions.ohlc.dataGrouping.groupPixelWidth + */ + + /** * The pixel width of the line/border. Defaults to `1`. * * @type {Number} - * @sample {highstock} stock/plotoptions/ohlc-linewidth/ A greater line width + * @sample {highstock} stock/plotoptions/ohlc-linewidth/ + * A greater line width * @default 1 * @product highstock */ lineWidth: 1, - /** - */ tooltip: { - /** - */ - pointFormat: '<span style="color:{point.color}">\u25CF</span> <b> {series.name}</b><br/>' + + pointFormat: '<span style="color:{point.color}">\u25CF</span> <b> {series.name}</b><br/>' + // eslint-disable-line max-len 'Open: {point.open}<br/>' + 'High: {point.high}<br/>' + 'Low: {point.low}<br/>' + 'Close: {point.close}<br/>' }, - /** - */ threshold: null, - /** - */ states: { /** * @extends plotOptions.column.states.hover * @product highstock */ hover: { /** - * The pixel width of the line representing the OHLC point. Defaults - * to `3`. + * The pixel width of the line representing the OHLC point. * * @type {Number} * @default 3 * @product highstock */ lineWidth: 3 } }, + /** + * Line color for up points. + * + * @type {Color} + * @product highstock + * @apioption plotOptions.ohlc.upColor */ - stickyTracking: true - //upColor: undefined + + stickyTracking: true + }, /** @lends seriesTypes.ohlc */ { directTouch: false, - pointArrayMap: ['open', 'high', 'low', 'close'], // array point configs are mapped to this + pointArrayMap: ['open', 'high', 'low', 'close'], toYData: function(point) { // return a plain array for speedy calculation return [point.open, point.high, point.low, point.close]; }, pointValKey: 'close', @@ -1920,24 +2229,33 @@ */ translate: function() { var series = this, yAxis = series.yAxis, hasModifyValue = !!series.modifyValue, - translated = ['plotOpen', 'plotHigh', 'plotLow', 'plotClose', 'yBottom']; // translate OHLC for + translated = [ + 'plotOpen', + 'plotHigh', + 'plotLow', + 'plotClose', + 'yBottom' + ]; // translate OHLC for seriesTypes.column.prototype.translate.apply(series); // Do the translation each(series.points, function(point) { - each([point.open, point.high, point.low, point.close, point.low], function(value, i) { - if (value !== null) { - if (hasModifyValue) { - value = series.modifyValue(value); + each( + [point.open, point.high, point.low, point.close, point.low], + function(value, i) { + if (value !== null) { + if (hasModifyValue) { + value = series.modifyValue(value); + } + point[translated[i]] = yAxis.toPixels(value, true); } - point[translated[i]] = yAxis.toPixels(value, true); } - }); + ); // Align the tooltip to the high value to avoid covering the point point.tooltipPos[1] = point.plotHigh + yAxis.pos - series.chart.plotTop; }); @@ -1969,11 +2287,13 @@ point.graphic = graphic = chart.renderer.path() .add(series.group); } - graphic.attr(series.pointAttribs(point, point.selected && 'select')); // #3897 + graphic.attr( + series.pointAttribs(point, point.selected && 'select') + ); // #3897 // crisp vector coordinates crispCorr = (graphic.strokeWidth() % 2) / 2; crispX = Math.round(point.plotX) - crispCorr; // #2596 @@ -2035,17 +2355,102 @@ /** * Extend the parent method by adding up or down to the class name. */ getClassName: function() { return Point.prototype.getClassName.call(this) + - (this.open < this.close ? ' highcharts-point-up' : ' highcharts-point-down'); + ( + this.open < this.close ? + ' highcharts-point-up' : + ' highcharts-point-down' + ); } }); - /* **************************************************************************** - * End OHLC series code * - *****************************************************************************/ + /** + * A `ohlc` series. If the [type](#series.ohlc.type) option is not + * specified, it is inherited from [chart.type](#chart.type). + * + * For options that apply to multiple series, it is recommended to add + * them to the [plotOptions.series](#plotOptions.series) options structure. + * To apply to all series of this specific type, apply it to [plotOptions. + * ohlc](#plotOptions.ohlc). + * + * @type {Object} + * @extends series,plotOptions.ohlc + * @excluding dataParser,dataURL + * @product highstock + * @apioption series.ohlc + */ + + /** + * An array of data points for the series. For the `ohlc` series type, + * points can be given in the following ways: + * + * 1. An array of arrays with 5 or 4 values. In this case, the values + * correspond to `x,open,high,low,close`. If the first value is a string, + * it is applied as the name of the point, and the `x` value is inferred. + * The `x` value can also be omitted, in which case the inner arrays + * should be of length 4\. Then the `x` value is automatically calculated, + * either starting at 0 and incremented by 1, or from `pointStart` + * and `pointInterval` given in the series options. + * + * ```js + * data: [ + * [0, 6, 5, 6, 7], + * [1, 9, 4, 8, 2], + * [2, 6, 3, 4, 10] + * ] + * ``` + * + * 2. An array of objects with named values. The objects are point + * configuration objects as seen below. If the total number of data + * points exceeds the series' [turboThreshold](#series.ohlc.turboThreshold), + * this option is not available. + * + * ```js + * data: [{ + * x: 1, + * open: 3, + * high: 4, + * low: 5, + * close: 2, + * name: "Point2", + * color: "#00FF00" + * }, { + * x: 1, + * open: 4, + * high: 3, + * low: 6, + * close: 7, + * name: "Point1", + * color: "#FF00FF" + * }] + * ``` + * + * @type {Array<Object|Array>} + * @extends series.arearange.data + * @excluding y,marker + * @product highstock + * @apioption series.ohlc.data + */ + + /** + * The closing value of each data point. + * + * @type {Number} + * @product highstock + * @apioption series.ohlc.data.close + */ + + /** + * The opening value of each data point. + * + * @type {Number} + * @product highstock + * @apioption series.ohlc.data.open + */ + }(Highcharts)); (function(H) { /** * (c) 2010-2017 Torstein Honsi * @@ -2056,68 +2461,69 @@ merge = H.merge, seriesType = H.seriesType, seriesTypes = H.seriesTypes; /** + * A candlestick chart is a style of financial chart used to describe price + * movements over time. + * + * @sample stock/demo/candlestick/ Candlestick chart + * * @extends {plotOptions.ohlc} - * @products highstock + * @excluding borderColor,borderRadius,borderWidth + * @product highstock * @optionparent plotOptions.candlestick */ var candlestickOptions = { - /** - */ states: { /** * @extends plotOptions.column.states.hover * @product highstock */ hover: { /** - * The pixel width of the line/border around the candlestick. Defaults - * to `2`. + * The pixel width of the line/border around the candlestick. * * @type {Number} * @default 2 * @product highstock */ lineWidth: 2 } }, /** + * @extends {plotOptions.ohlc.tooltip} */ tooltip: defaultPlotOptions.ohlc.tooltip, - /** - */ threshold: null, /** * The color of the line/border of the candlestick. * - * In [styled mode](http://www.highcharts.com/docs/chart-design-and- - * style/style-by-css), the line stroke can be set with the `.highcharts- + * In styled mode, the line stroke can be set with the `.highcharts- * candlestick-series .highcahrts-point` rule. * * @type {Color} * @see [upLineColor](#plotOptions.candlestick.upLineColor) - * @sample {highstock} stock/plotoptions/candlestick-linecolor/ Candlestick line colors + * @sample {highstock} stock/plotoptions/candlestick-linecolor/ + * Candlestick line colors * @default #000000 * @product highstock */ lineColor: '#000000', /** * The pixel width of the candlestick line/border. Defaults to `1`. * * - * In [styled mode](http://www.highcharts.com/docs/chart-design-and- - * style/style-by-css), the line stroke width can be set with the `. + * In styled mode, the line stroke width can be set with the `. * highcharts-candlestick-series .highcahrts-point` rule. * * @type {Number} * @default 1 * @product highstock @@ -2125,28 +2531,41 @@ lineWidth: 1, /** * The fill color of the candlestick when values are rising. * - * In [styled mode](http://www.highcharts.com/docs/chart-design-and- - * style/style-by-css), the up color can be set with the `.highcharts- + * In styled mode, the up color can be set with the `.highcharts- * candlestick-series .highcharts-point-up` rule. * * @type {Color} * @sample {highstock} stock/plotoptions/candlestick-color/ Custom colors * @sample {highstock} highcharts/css/candlestick/ Colors in styled mode * @default #ffffff * @product highstock */ upColor: '#ffffff', + stickyTracking: true + /** + * The specific line color for up candle sticks. The default is to inherit + * the general `lineColor` setting. + * + * @type {Color} + * @sample {highstock} stock/plotoptions/candlestick-linecolor/ Candlestick line colors + * @default null + * @since 1.3.6 + * @product highstock + * @apioption plotOptions.candlestick.upLineColor */ - stickyTracking: true - // upLineColor: null + /** + * @default ohlc + * @apioption plotOptions.candlestick.dataGrouping.approximation + */ + }; /** * The candlestick series type. * @@ -2188,11 +2607,11 @@ /** * Draw the data points */ drawPoints: function() { - var series = this, //state = series.state, + var series = this, points = series.points, chart = series.chart; each(points, function(point) { @@ -2272,51 +2691,266 @@ } }); - /* **************************************************************************** - * End Candlestick series code * - *****************************************************************************/ + /** + * A `candlestick` series. If the [type](#series.candlestick.type) + * option is not specified, it is inherited from [chart.type](#chart. + * type). + * + * For options that apply to multiple series, it is recommended to add + * them to the [plotOptions.series](#plotOptions.series) options structure. + * To apply to all series of this specific type, apply it to [plotOptions. + * candlestick](#plotOptions.candlestick). + * + * @type {Object} + * @extends series,plotOptions.candlestick + * @excluding dataParser,dataURL + * @product highstock + * @apioption series.candlestick + */ + /** + * An array of data points for the series. For the `candlestick` series + * type, points can be given in the following ways: + * + * 1. An array of arrays with 5 or 4 values. In this case, the values + * correspond to `x,open,high,low,close`. If the first value is a string, + * it is applied as the name of the point, and the `x` value is inferred. + * The `x` value can also be omitted, in which case the inner arrays + * should be of length 4\. Then the `x` value is automatically calculated, + * either starting at 0 and incremented by 1, or from `pointStart` + * and `pointInterval` given in the series options. + * + * ```js + * data: [ + * [0, 7, 2, 0, 4], + * [1, 1, 4, 2, 8], + * [2, 3, 3, 9, 3] + * ] + * ``` + * + * 2. An array of objects with named values. The objects are point + * configuration objects as seen below. If the total number of data + * points exceeds the series' [turboThreshold](#series.candlestick. + * turboThreshold), this option is not available. + * + * ```js + * data: [{ + * x: 1, + * open: 9, + * high: 2, + * low: 4, + * close: 6, + * name: "Point2", + * color: "#00FF00" + * }, { + * x: 1, + * open: 1, + * high: 4, + * low: 7, + * close: 7, + * name: "Point1", + * color: "#FF00FF" + * }] + * ``` + * + * @type {Array<Object|Array>} + * @extends series.ohlc.data + * @excluding y + * @product highstock + * @apioption series.candlestick.data + */ + }(Highcharts)); - (function(H) { + var onSeriesMixin = (function(H) { /** * (c) 2010-2017 Torstein Honsi * * License: www.highcharts.com/license */ + + var each = H.each, + seriesTypes = H.seriesTypes, + stableSort = H.stableSort; + + var onSeriesMixin = { + /** + * Extend the translate method by placing the point on the related series + */ + translate: function() { + + seriesTypes.column.prototype.translate.apply(this); + + var series = this, + options = series.options, + chart = series.chart, + points = series.points, + cursor = points.length - 1, + point, + lastPoint, + optionsOnSeries = options.onSeries, + onSeries = optionsOnSeries && chart.get(optionsOnSeries), + onKey = options.onKey || 'y', + step = onSeries && onSeries.options.step, + onData = onSeries && onSeries.points, + i = onData && onData.length, + xAxis = series.xAxis, + yAxis = series.yAxis, + xAxisExt = xAxis.getExtremes(), + xOffset = 0, + leftPoint, + lastX, + rightPoint, + currentDataGrouping; + + // relate to a master series + if (onSeries && onSeries.visible && i) { + xOffset = (onSeries.pointXOffset || 0) + (onSeries.barW || 0) / 2; + currentDataGrouping = onSeries.currentDataGrouping; + lastX = ( + onData[i - 1].x + + (currentDataGrouping ? currentDataGrouping.totalRange : 0) + ); // #2374 + + // sort the data points + stableSort(points, function(a, b) { + return (a.x - b.x); + }); + + onKey = 'plot' + onKey[0].toUpperCase() + onKey.substr(1); + while (i-- && points[cursor]) { + point = points[cursor]; + leftPoint = onData[i]; + if (leftPoint.x <= point.x && leftPoint[onKey] !== undefined) { + if (point.x <= lastX) { // #803 + + point.plotY = leftPoint[onKey]; + + // interpolate between points, #666 + if (leftPoint.x < point.x && !step) { + rightPoint = onData[i + 1]; + if (rightPoint && rightPoint[onKey] !== undefined) { + point.plotY += + // the distance ratio, between 0 and 1 + ( + (point.x - leftPoint.x) / + (rightPoint.x - leftPoint.x) + ) * + // the y distance + (rightPoint[onKey] - leftPoint[onKey]); + } + } + } + cursor--; + i++; // check again for points in the same x position + if (cursor < 0) { + break; + } + } + } + } + + // Add plotY position and handle stacking + each(points, function(point, i) { + + var stackIndex; + + // Undefined plotY means the point is either on axis, outside series + // range or hidden series. If the series is outside the range of the + // x axis it should fall through with an undefined plotY, but then + // we must remove the shapeArgs (#847). + if (point.plotY === undefined) { + if (point.x >= xAxisExt.min && point.x <= xAxisExt.max) { + // we're inside xAxis range + point.plotY = chart.chartHeight - xAxis.bottom - + (xAxis.opposite ? xAxis.height : 0) + + xAxis.offset - yAxis.top; // #3517 + } else { + point.shapeArgs = {}; // 847 + } + } + point.plotX += xOffset; // #2049 + // if multiple flags appear at the same x, order them into a stack + lastPoint = points[i - 1]; + if (lastPoint && lastPoint.plotX === point.plotX) { + if (lastPoint.stackIndex === undefined) { + lastPoint.stackIndex = 0; + } + stackIndex = lastPoint.stackIndex + 1; + } + point.stackIndex = stackIndex; // #3639 + }); + + + } + }; + return onSeriesMixin; + }(Highcharts)); + (function(H, onSeriesMixin) { + /** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ var addEvent = H.addEvent, each = H.each, merge = H.merge, noop = H.noop, Renderer = H.Renderer, Series = H.Series, seriesType = H.seriesType, - seriesTypes = H.seriesTypes, SVGRenderer = H.SVGRenderer, TrackerMixin = H.TrackerMixin, VMLRenderer = H.VMLRenderer, - symbols = SVGRenderer.prototype.symbols, - stableSort = H.stableSort; + symbols = SVGRenderer.prototype.symbols; /** - * The flags series type. - * + * The Flags series. * @constructor seriesTypes.flags * @augments seriesTypes.column */ /** + * Flags are used to mark events in stock charts. They can be added on the + * timeline, or attached to a specific series. + * + * @sample stock/demo/flags-general/ Flags on a line series * @extends {plotOptions.column} + * @excluding animation,borderColor,borderRadius,borderWidth,colorByPoint,dataGrouping,pointPadding,pointWidth,turboThreshold + * @product highstock * @optionparent plotOptions.flags */ seriesType('flags', 'column', { /** + * In case the flag is placed on a series, on what point key to place + * it. Line and columns have one key, `y`. In range or OHLC-type series, + * however, the flag can optionally be placed on the `open`, `high`, + * `low` or `close` key. + * + * @validvalue ["y", "open", "high", "low", "close"] + * @type {String} + * @sample {highstock} stock/plotoptions/flags-onkey/ Range series, flag on high + * @default y + * @since 4.2.2 + * @product highstock + * @apioption plotOptions.flags.onKey */ + + /** + * The id of the series that the flags should be drawn on. If no id + * is given, the flags are drawn on the x axis. + * + * @type {String} + * @sample {highstock} stock/plotoptions/flags/ Flags on series and on x axis + * @default undefined + * @product highstock + * @apioption plotOptions.flags.onSeries + */ + pointRange: 0, // #673 - //radius: 2, /** * The shape of the marker. Can be one of "flag", "circlepin", "squarepin", * or an image on the format `url(/path-to-image.jpg)`. Individual * shapes can also be set for each point. @@ -2361,19 +2995,24 @@ * @extends plotOptions.series.tooltip * @excluding changeDecimals,valueDecimals,valuePrefix,valueSuffix * @product highstock */ tooltip: { - - /** - */ pointFormat: '{point.text}<br/>' }, + threshold: null, + /** + * The text to display on each flag. This can be defined on series level, + * or individually for each point. Defaults to `"A"`. + * + * @type {String} + * @default "A" + * @product highstock + * @apioption plotOptions.flags.title */ - threshold: null, /** * The y position of the top left corner of the flag relative to either * the series (if onSeries is defined), or the x axis. Defaults to * `-30`. @@ -2382,46 +3021,70 @@ * @default -30 * @product highstock */ y: -30, + /** + * Whether to use HTML to render the flag texts. Using HTML allows for + * advanced formatting, images and reliable bi-directional text rendering. + * Note that exported images won't respect the HTML, and that HTML + * won't respect Z-index settings. + * + * @type {Boolean} + * @default false + * @since 1.3 + * @product highstock + * @apioption plotOptions.flags.useHTML + */ + + /** + * The fill color for the flags. */ fillColor: '#ffffff', - // lineColor: color, /** - * The pixel width of the candlestick line/border. Defaults to `1`. + * The color of the line/border of the flag. * + * In styled mode, the stroke is set in the `.highcharts-flag-series + * .highcharts-point` rule. + * + * @type {Color} + * @default #000000 + * @product highstock + * @apioption plotOptions.flags.lineColor + */ + + /** + * The pixel width of the flag's line/border. + * * @type {Number} * @default 1 * @product highstock */ lineWidth: 1, - /** - */ states: { /** * @extends plotOptions.column.states.hover * @product highstock */ hover: { /** - * The color of the line/border of the flag Defaults to `"black"`. + * The color of the line/border of the flag. * * @type {String} * @default "black" * @product highstock */ lineColor: '#000000', /** - * The fill or background color of the flag Defaults to `"#FCFFC5"`. + * The fill or background color of the flag. * * @type {String} * @default "#FCFFC5" * @product highstock */ @@ -2430,26 +3093,19 @@ }, /** * The text styles of the flag. * - * In [styled mode](http://www.highcharts.com/docs/chart-design-and- - * style/style-by-css), the styles are set in the `.highcharts-flag- + * In styled mode, the styles are set in the `.highcharts-flag- * series .highcharts-point` rule. * * @type {CSSObject} * @default { "fontSize": "11px", "fontWeight": "bold" } * @product highstock */ style: { - - /** - */ fontSize: '11px', - - /** - */ fontWeight: 'bold' } }, /** @lends seriesTypes.flags.prototype */ { @@ -2487,112 +3143,12 @@ 'stroke-width': lineWidth || options.lineWidth || 0 }; }, - /** - * Extend the translate method by placing the point on the related series - */ - translate: function() { + translate: onSeriesMixin.translate, - seriesTypes.column.prototype.translate.apply(this); - - var series = this, - options = series.options, - chart = series.chart, - points = series.points, - cursor = points.length - 1, - point, - lastPoint, - optionsOnSeries = options.onSeries, - onSeries = optionsOnSeries && chart.get(optionsOnSeries), - onKey = options.onKey || 'y', - step = onSeries && onSeries.options.step, - onData = onSeries && onSeries.points, - i = onData && onData.length, - xAxis = series.xAxis, - yAxis = series.yAxis, - xAxisExt = xAxis.getExtremes(), - xOffset = 0, - leftPoint, - lastX, - rightPoint, - currentDataGrouping; - - // relate to a master series - if (onSeries && onSeries.visible && i) { - xOffset = (onSeries.pointXOffset || 0) + (onSeries.barW || 0) / 2; - currentDataGrouping = onSeries.currentDataGrouping; - lastX = onData[i - 1].x + (currentDataGrouping ? currentDataGrouping.totalRange : 0); // #2374 - - // sort the data points - stableSort(points, function(a, b) { - return (a.x - b.x); - }); - - onKey = 'plot' + onKey[0].toUpperCase() + onKey.substr(1); - while (i-- && points[cursor]) { - point = points[cursor]; - leftPoint = onData[i]; - if (leftPoint.x <= point.x && leftPoint[onKey] !== undefined) { - if (point.x <= lastX) { // #803 - - point.plotY = leftPoint[onKey]; - - // interpolate between points, #666 - if (leftPoint.x < point.x && !step) { - rightPoint = onData[i + 1]; - if (rightPoint && rightPoint[onKey] !== undefined) { - point.plotY += - ((point.x - leftPoint.x) / (rightPoint.x - leftPoint.x)) * // the distance ratio, between 0 and 1 - (rightPoint[onKey] - leftPoint[onKey]); // the y distance - } - } - } - cursor--; - i++; // check again for points in the same x position - if (cursor < 0) { - break; - } - } - } - } - - // Add plotY position and handle stacking - each(points, function(point, i) { - - var stackIndex; - - // Undefined plotY means the point is either on axis, outside series - // range or hidden series. If the series is outside the range of the - // x axis it should fall through with an undefined plotY, but then - // we must remove the shapeArgs (#847). - if (point.plotY === undefined) { - if (point.x >= xAxisExt.min && point.x <= xAxisExt.max) { - // we're inside xAxis range - point.plotY = chart.chartHeight - xAxis.bottom - - (xAxis.opposite ? xAxis.height : 0) + - xAxis.offset - yAxis.top; // #3517 - } else { - point.shapeArgs = {}; // 847 - } - } - point.plotX += xOffset; // #2049 - // if multiple flags appear at the same x, order them into a stack - lastPoint = points[i - 1]; - if (lastPoint && lastPoint.plotX === point.plotX) { - if (lastPoint.stackIndex === undefined) { - lastPoint.stackIndex = 0; - } - stackIndex = lastPoint.stackIndex + 1; - } - point.stackIndex = stackIndex; // #3639 - }); - - - }, - /** * Draw the markers */ drawPoints: function() { var series = this, @@ -2797,15 +3353,81 @@ each(['flag', 'circlepin', 'squarepin'], function(shape) { VMLRenderer.prototype.symbols[shape] = symbols[shape]; }); } - /* **************************************************************************** - * End Flags series code * - *****************************************************************************/ - }(Highcharts)); + /** + * A `flags` series. If the [type](#series.flags.type) option is not + * specified, it is inherited from [chart.type](#chart.type). + * + * For options that apply to multiple series, it is recommended to add + * them to the [plotOptions.series](#plotOptions.series) options structure. + * To apply to all series of this specific type, apply it to [plotOptions. + * flags](#plotOptions.flags). + * + * @type {Object} + * @extends series,plotOptions.flags + * @excluding dataParser,dataURL + * @product highstock + * @apioption series.flags + */ + + /** + * An array of data points for the series. For the `flags` series type, + * points can be given in the following ways: + * + * 1. An array of objects with named values. The objects are point + * configuration objects as seen below. If the total number of data + * points exceeds the series' [turboThreshold](#series.flags.turboThreshold), + * this option is not available. + * + * ```js + * data: [{ + * x: 1, + * title: "A", + * text: "First event" + * }, { + * x: 1, + * title: "B", + * text: "Second event" + * }]</pre> + * + * @type {Array<Object>} + * @extends series.line.data + * @excluding y,dataLabels,marker,name + * @product highstock + * @apioption series.flags.data + */ + + /** + * The fill color of an individual flag. By default it inherits from + * the series color. + * + * @type {Color} + * @product highstock + * @apioption series.flags.data.fillColor + */ + + /** + * The longer text to be shown in the flag's tooltip. + * + * @type {String} + * @product highstock + * @apioption series.flags.data.text + */ + + /** + * The short text to be shown on the flag. + * + * @type {String} + * @product highstock + * @apioption series.flags.data.title + */ + + + }(Highcharts, onSeriesMixin)); (function(H) { /** * (c) 2010-2017 Torstein Honsi * * License: www.highcharts.com/license @@ -2827,24 +3449,21 @@ wrap = H.wrap, swapXY; /** * - * The scrollbar is a means of panning over the X axis of a chart. + * The scrollbar is a means of panning over the X axis of a stock chart. * - * In [styled mode](http://www.highcharts.com/docs/chart-design- - * and-style/style-by-css), all the presentational options for the - * scrollbar are replaced by the classes `.highcharts-scrollbar- - * thumb`, `.highcharts-scrollbar-arrow`, `.highcharts-scrollbar- - * button`, `.highcharts-scrollbar-rifles` and `.highcharts-scrollbar- - * track`. + * In styled mode, all the presentational options for the + * scrollbar are replaced by the classes `.highcharts-scrollbar-thumb`, + * `.highcharts-scrollbar-arrow`, `.highcharts-scrollbar-button`, + * `.highcharts-scrollbar-rifles` and `.highcharts-scrollbar-track`. * * @product highstock * @optionparent scrollbar */ var defaultScrollbarOptions = { - //enabled: true /** * The height of the scrollbar. The height also applies to the width * of the scroll arrows so that they are always squares. Defaults to * 20 for touch devices and 14 for mouse devices. @@ -2852,11 +3471,10 @@ * @type {Number} * @sample {highstock} stock/scrollbar/height/ A 30px scrollbar * @product highstock */ height: isTouchDevice ? 20 : 14, - // trackBorderRadius: 0 /** * The border rounding radius of the bar. * * @type {Number} @@ -2886,10 +3504,12 @@ * @product highstock */ liveRedraw: svg && !isTouchDevice, /** + * The margin between the scrollbar and its axis when the scrollbar is + * applied directly to an axis. */ margin: 10, /** * The minimum width of the scrollbar. @@ -2898,18 +3518,15 @@ * @default 6 * @since 1.2.5 * @product highstock */ minWidth: 6, - //showFull: true, - //size: null, - /** - */ step: 0.2, /** + * The z index of the scrollbar group. */ zIndex: 3, /** @@ -3733,13 +4350,42 @@ /** * (c) 2010-2017 Torstein Honsi * * License: www.highcharts.com/license */ - /* **************************************************************************** - * Start Navigator code * - *****************************************************************************/ + /* eslint max-len: ["warn", 80, 4] */ + + /** + * Options for the corresponding navigator series if `showInNavigator` + * is `true` for this series. Available options are the same as any + * series, documented at [plotOptions](#plotOptions.series) and + * [series](#series). + * + * + * These options are merged with options in [navigator.series](#navigator. + * series), and will take precedence if the same option is defined both + * places. + * + * @type {Object} + * @see [navigator.series](#navigator.series) + * @default undefined + * @since 5.0.0 + * @product highstock + * @apioption plotOptions.series.navigatorOptions + */ + + /** + * Whether or not to show the series in the navigator. Takes precedence + * over [navigator.baseSeries](#navigator.baseSeries) if defined. + * + * @type {Boolean} + * @default undefined + * @since 5.0.0 + * @product highstock + * @apioption plotOptions.series.showInNavigator + */ + var addEvent = H.addEvent, Axis = H.Axis, Chart = H.Chart, color = H.color, defaultDataGroupingUnits = H.defaultDataGroupingUnits, @@ -3760,18 +4406,17 @@ removeEvent = H.removeEvent, Scrollbar = H.Scrollbar, Series = H.Series, seriesTypes = H.seriesTypes, wrap = H.wrap, - swapXY = H.swapXY, units = [].concat(defaultDataGroupingUnits), // copy defaultSeriesType, - // Finding the min or max of a set of variables where we don't know if they are defined, - // is a pattern that is repeated several places in Highcharts. Consider making this - // a global utility method. + // Finding the min or max of a set of variables where we don't know if they + // are defined, is a pattern that is repeated several places in Highcharts. + // Consider making this a global utility method. numExt = function(extreme) { var numbers = grep(arguments, isNumber); if (numbers.length) { return Math[extreme].apply(0, numbers); } @@ -3779,122 +4424,190 @@ // add more resolution to units units[4] = ['day', [1, 2, 3, 4]]; // allow more days units[5] = ['week', [1, 2, 3]]; // allow more weeks - defaultSeriesType = seriesTypes.areaspline === undefined ? 'line' : 'areaspline'; + defaultSeriesType = seriesTypes.areaspline === undefined ? + 'line' : + 'areaspline'; extend(defaultOptions, { /** * The navigator is a small series below the main series, displaying * a view of the entire data set. It provides tools to zoom in and * out on parts of the data as well as panning across the dataset. * - * @optionparent navigator * @product highstock + * @optionparent navigator */ navigator: { - //enabled: true, - /** * The height of the navigator. - * + * * @type {Number} * @sample {highstock} stock/navigator/height/ A higher navigator * @default 40 * @product highstock */ height: 40, /** * The distance from the nearest element, the X axis or X axis labels. - * + * * @type {Number} - * @sample {highstock} stock/navigator/margin/ A margin of 2 draws the navigator closer to the X axis labels + * @sample {highstock} stock/navigator/margin/ + * A margin of 2 draws the navigator closer to the X axis labels * @default 25 * @product highstock */ margin: 25, /** * Whether the mask should be inside the range marking the zoomed * range, or outside. In Highstock 1.x it was always `false`. - * + * * @type {Boolean} - * @sample {highstock} stock/navigator/maskinside-false/ False, mask outside + * @sample {highstock} stock/navigator/maskinside-false/ + * False, mask outside * @default true * @since 2.0 * @product highstock */ maskInside: true, - /** - * Options for the handles for dragging the zoomed area. Available - * options are `backgroundColor` (defaults to `#ebe7e8`) and `borderColor` - * (defaults to `#b2b1b6`). - * + * Options for the handles for dragging the zoomed area. + * * @type {Object} * @sample {highstock} stock/navigator/handles/ Colored handles - * @sample {highstock} stock/navigator/handles/ Colored handles * @product highstock */ handles: { + /** + * Width for handles. + * + * @type {umber} + * @default 7 + * @product highstock + * @sample {highstock} stock/navigator/styled-handles/ + * Styled handles + * @since 6.0.0 + */ + width: 7, /** + * Height for handles. + * + * @type {Number} + * @default 15 + * @product highstock + * @sample {highstock} stock/navigator/styled-handles/ + * Styled handles + * @since 6.0.0 + */ + height: 15, + + /** + * Array to define shapes of handles. 0-index for left, 1-index for + * right. + * + * Additionally, the URL to a graphic can be given on this form: + * `url(graphic.png)`. Note that for the image to be applied to + * exported charts, its URL needs to be accessible by the export + * server. + * + * Custom callbacks for symbol path generation can also be added to + * `Highcharts.SVGRenderer.prototype.symbols`. The callback is then + * used by its method name, as shown in the demo. + * + * @type {Array} + * @default ['navigator-handle', 'navigator-handle'] + * @product highstock + * @sample {highstock} stock/navigator/styled-handles/ + * Styled handles + * @since 6.0.0 + */ + symbols: ['navigator-handle', 'navigator-handle'], + + /** + * Allows to enable/disable handles. + * + * @type {Boolean} + * @default true + * @product highstock + * @since 6.0.0 + */ + enabled: true, + + + /** + * The width for the handle border and the stripes inside. + * + * @type {Number} + * @default 7 + * @product highstock + * @sample {highstock} stock/navigator/styled-handles/ + * Styled handles + * @since 6.0.0 + */ + lineWidth: 1, + + /** * The fill for the handle. - * + * * @type {Color} - * @default #f2f2f2 * @product highstock */ backgroundColor: '#f2f2f2', /** * The stroke for the handle border and the stripes inside. - * + * * @type {Color} - * @default #999999 * @product highstock */ borderColor: '#999999' + + }, + + /** * The color of the mask covering the areas of the navigator series * that are currently not visible in the main series. The default * color is bluish with an opacity of 0.3 to see the series below. - * + * * @type {Color} - * @see In [styled mode](http://www.highcharts.com/docs/chart-design-and- - * style/style-by-css), the mask is styled with the `.highcharts-navigator- - * mask` and `.highcharts-navigator-mask-inside` classes. - * @sample {highstock} stock/navigator/maskfill/ Blue, semi transparent mask + * @see In styled mode, the mask is styled with the + * `.highcharts-navigator-mask` and + * `.highcharts-navigator-mask-inside` classes. + * @sample {highstock} stock/navigator/maskfill/ + * Blue, semi transparent mask * @default rgba(102,133,194,0.3) * @product highstock */ maskFill: color('#6685c2').setOpacity(0.3).get(), /** * The color of the line marking the currently zoomed area in the * navigator. - * + * * @type {Color} * @sample {highstock} stock/navigator/outline/ 2px blue outline * @default #cccccc * @product highstock */ outlineColor: '#cccccc', /** * The width of the line marking the currently zoomed area in the * navigator. - * + * * @type {Number} - * @see In [styled mode](http://www.highcharts.com/docs/chart-design-and- - * style/style-by-css), the outline stroke width is set with the `. + * @see In styled mode, the outline stroke width is set with the `. * highcharts-navigator-outline` class. * @sample {highstock} stock/navigator/outline/ 2px blue outline * @default 2 * @product highstock */ @@ -3903,304 +4616,263 @@ /** * Options for the navigator series. Available options are the same * as any series, documented at [plotOptions](#plotOptions.series) * and [series](#series). - * + * * Unless data is explicitly defined on navigator.series, the data * is borrowed from the first series in the chart. - * + * * Default series options for the navigator series are: - * + * * <pre>series: { - * type: 'areaspline', - * color: '#4572A7', - * fillOpacity: 0.05, - * dataGrouping: { - * smoothed: true - * }, - * lineWidth: 1, - * marker: { - * enabled: false - * } + * type: 'areaspline', + * fillOpacity: 0.05, + * dataGrouping: { + * smoothed: true + * }, + * lineWidth: 1, + * marker: { + * enabled: false + * } * }</pre> - * + * * @type {Object} - * @see In [styled mode](http://www.highcharts.com/docs/chart-design-and- - * style/style-by-css), the navigator series is styled with the `. + * @see In styled mode, the navigator series is styled with the `. * highcharts-navigator-series` class. - * @sample {highstock} stock/navigator/series-data/ Using a separate data set for the navigator - * @sample {highstock} stock/navigator/series/ A green navigator series + * @sample {highstock} stock/navigator/series-data/ + * Using a separate data set for the navigator + * @sample {highstock} stock/navigator/series/ + * A green navigator series * @product highstock */ series: { /** + * The type of the navigator series. Defaults to `areaspline` if + * defined, otherwise `line`. + * + * @type {String} */ type: defaultSeriesType, - /** - */ - color: '#335cad', /** + * The fill opacity of the navigator series. */ fillOpacity: 0.05, /** + * The pixel line width of the navigator series. */ lineWidth: 1, /** + * @ignore */ compare: null, /** + * Data grouping options for the navigator series. + * + * @extends {plotOptions.series.dataGrouping} */ dataGrouping: { - - /** - */ approximation: 'average', - - /** - */ enabled: true, - - /** - */ groupPixelWidth: 2, - - /** - */ smoothed: true, - - /** - */ units: units }, /** + * Data label options for the navigator series. Data labels are + * disabled by default on the navigator series. + * + * @extends {plotOptions.series.dataLabels} */ dataLabels: { - - /** - */ enabled: false, - - /** - */ zIndex: 2 // #1839 }, - /** - */ id: 'highcharts-navigator-series', - - /** - */ className: 'highcharts-navigator-series', /** + * Line color for the navigator series. Allows setting the color + * while disallowing the default candlestick setting. + * + * @type {Color} */ - lineColor: null, // Allow color setting while disallowing default candlestick setting (#4602) + lineColor: null, // #4602 - /** - */ marker: { - - /** - */ enabled: false }, - /** - */ pointRange: 0, - /** + * The threshold option. Setting it to 0 will make the default + * navigator area series draw its area from the 0 value and up. + * @type {Number} */ - shadow: false, - - /** - */ threshold: null }, - //top: undefined, - //opposite: undefined, /** - * Options for the navigator X axis. Available options are the same - * as any X axis, documented at [xAxis](#xAxis). Default series options + * Options for the navigator X axis. Default series options * for the navigator xAxis are: - * + * * <pre>xAxis: { - * tickWidth: 0, - * lineWidth: 0, - * gridLineWidth: 1, - * tickPixelInterval: 200, - * labels: { - * align: 'left', - * style: { - * color: '#888' - * }, - * x: 3, - * y: -4 - * } + * tickWidth: 0, + * lineWidth: 0, + * gridLineWidth: 1, + * tickPixelInterval: 200, + * labels: { + * align: 'left', + * style: { + * color: '#888' + * }, + * x: 3, + * y: -4 + * } * }</pre> - * + * * @type {Object} + * @extends {xAxis} + * @excluding linkedTo,maxZoom,minRange,opposite,range,scrollbar, + * showEmpty,maxRange * @product highstock */ xAxis: { - /** + * Additional range on the right side of the xAxis. Works similar to + * xAxis.maxPadding, but value is set in milliseconds. + * Can be set for both, main xAxis and navigator's xAxis. + * + * @type {Number} + * @default 0 + * @since 6.0.0 + * @product highstock + * @apioption xAxis.overscroll */ - className: 'highcharts-navigator-xaxis', + overscroll: 0, - /** - */ + className: 'highcharts-navigator-xaxis', tickLength: 0, - /** - */ lineWidth: 0, - - /** - */ gridLineColor: '#e6e6e6', - - /** - */ gridLineWidth: 1, - /** - */ tickPixelInterval: 200, - /** - */ labels: { - - /** - */ align: 'left', - /** - */ style: { - - /** - */ color: '#999999' }, - /** - */ x: 3, - - /** - */ y: -4 }, - /** - */ crosshair: false }, /** - * Options for the navigator Y axis. Available options are the same - * as any y axis, documented at [yAxis](#yAxis). Default series options + * Options for the navigator Y axis. Default series options * for the navigator yAxis are: - * + * * <pre>yAxis: { - * gridLineWidth: 0, - * startOnTick: false, - * endOnTick: false, - * minPadding: 0.1, - * maxPadding: 0.1, - * labels: { - * enabled: false - * }, - * title: { - * text: null - * }, - * tickWidth: 0 + * gridLineWidth: 0, + * startOnTick: false, + * endOnTick: false, + * minPadding: 0.1, + * maxPadding: 0.1, + * labels: { + * enabled: false + * }, + * title: { + * text: null + * }, + * tickWidth: 0 * }</pre> - * + * * @type {Object} + * @extends {yAxis} + * @excluding height,linkedTo,maxZoom,minRange,ordinal,range,showEmpty, + * scrollbar,top,units,maxRange * @product highstock */ yAxis: { - /** - */ className: 'highcharts-navigator-yaxis', - /** - */ gridLineWidth: 0, - /** - */ startOnTick: false, - - /** - */ endOnTick: false, - - /** - */ minPadding: 0.1, - - /** - */ maxPadding: 0.1, - - /** - */ labels: { - - /** - */ enabled: false }, - - /** - */ crosshair: false, - - /** - */ title: { - - /** - */ text: null }, - - /** - */ tickLength: 0, - - /** - */ tickWidth: 0 } } }); /** + * Draw one of the handles on the side of the zoomed range in the navigator + * @param {Boolean} inverted flag for chart.inverted + * @returns {Array} Path to be used in a handle + */ + H.Renderer.prototype.symbols['navigator-handle'] = function( + x, + y, + w, + h, + options + ) { + var halfWidth = options.width / 2, + markerPosition = Math.round(halfWidth / 3) + 0.5, + height = options.height; + + return [ + 'M', -halfWidth - 1, 0.5, + 'L', + halfWidth, 0.5, + 'L', + halfWidth, height + 0.5, + 'L', -halfWidth - 1, height + 0.5, + 'L', -halfWidth - 1, 0.5, + 'M', -markerPosition, 4, + 'L', -markerPosition, height - 3, + 'M', + markerPosition - 1, 4, + 'L', + markerPosition - 1, height - 3 + ]; + }; + + /** * The Navigator class * @param {Object} chart - Chart object * @class */ function Navigator(chart) { @@ -4214,45 +4886,28 @@ * @param {Number} index 0 for left and 1 for right * @param {Boolean} inverted flag for chart.inverted * @param {String} verb use 'animate' or 'attr' */ drawHandle: function(x, index, inverted, verb) { - var navigator = this; + var navigator = this, + height = navigator.navigatorOptions.handles.height; // Place it navigator.handles[index][verb](inverted ? { - translateX: Math.round(navigator.left + navigator.height / 2 - 8), - translateY: Math.round(navigator.top + parseInt(x, 10) + 0.5) + translateX: Math.round(navigator.left + navigator.height / 2), + translateY: Math.round( + navigator.top + parseInt(x, 10) + 0.5 - height + ) } : { translateX: Math.round(navigator.left + parseInt(x, 10)), - translateY: Math.round(navigator.top + navigator.height / 2 - 8) + translateY: Math.round( + navigator.top + navigator.height / 2 - height / 2 - 1 + ) }); }, /** - * Draw one of the handles on the side of the zoomed range in the navigator - * @param {Boolean} inverted flag for chart.inverted - * @returns {Array} Path to be used in a handle - */ - getHandlePath: function(inverted) { - return swapXY([ - 'M', -4.5, 0.5, - 'L', - 3.5, 0.5, - 'L', - 3.5, 15.5, - 'L', -4.5, 15.5, - 'L', -4.5, 0.5, - 'M', -1.5, 4, - 'L', -1.5, 12, - 'M', - 0.5, 4, - 'L', - 0.5, 12 - ], inverted); - }, - /** * Render outline around the zoomed range * @param {Number} zoomedMin in pixels position where zoomed range starts * @param {Number} zoomedMax in pixels position where zoomed range ends * @param {Boolean} inverted flag if chart is inverted * @param {String} verb use 'animate' or 'attr' @@ -4356,11 +5011,11 @@ height, width, x, y; - // Determine rectangle position & size + // Determine rectangle position & size // According to (non)inverted position: if (inverted) { x = [left, left, left]; y = [top, top + zoomedMin, top + zoomedMax]; width = [navigatorHeight, navigatorHeight, navigatorHeight]; @@ -4444,33 +5099,42 @@ }) .add(navigatorGroup); // Create the handlers: - each([0, 1], function(index) { - navigator.handles[index] = renderer - .path(navigator.getHandlePath(inverted)) + if (navigatorOptions.handles.enabled) { + each([0, 1], function(index) { + navigatorOptions.handles.inverted = chart.inverted; + navigator.handles[index] = renderer.symbol( + navigatorOptions.handles.symbols[index], -navigatorOptions.handles.width / 2 - 1, + 0, + navigatorOptions.handles.width, + navigatorOptions.handles.height, + navigatorOptions.handles + ); // zIndex = 6 for right handle, 7 for left. // Can't be 10, because of the tooltip in inverted chart #2908 - .attr({ - zIndex: 7 - index - }) - .addClass( - 'highcharts-navigator-handle highcharts-navigator-handle-' + ['left', 'right'][index] - ).add(navigatorGroup); + navigator.handles[index].attr({ + zIndex: 7 - index + }) + .addClass( + 'highcharts-navigator-handle ' + + 'highcharts-navigator-handle-' + ['left', 'right'][index] + ).add(navigatorGroup); - var handlesOptions = navigatorOptions.handles; - navigator.handles[index] - .attr({ - fill: handlesOptions.backgroundColor, - stroke: handlesOptions.borderColor, - 'stroke-width': 1 - }) - .css(mouseCursor); + var handlesOptions = navigatorOptions.handles; + navigator.handles[index] + .attr({ + fill: handlesOptions.backgroundColor, + stroke: handlesOptions.borderColor, + 'stroke-width': handlesOptions.lineWidth + }) + .css(mouseCursor); - }); + }); + } }, /** * Update navigator * @param {Object} options Options to merge in when updating navigator @@ -4512,11 +5176,13 @@ rendered = navigator.rendered, inverted = chart.inverted, verb, newMin, newMax, - minRange = chart.xAxis[0].minRange; + currentRange, + minRange = chart.xAxis[0].minRange, + maxRange = chart.xAxis[0].options.maxRange; // Don't redraw while moving the handles (#4703). if (this.hasDragged && !defined(pxMin)) { return; } @@ -4539,11 +5205,12 @@ chart.plotLeft + scrollbarHeight + (inverted ? chart.plotWidth : 0) ); navigator.size = zoomedMax = navigatorSize = pick( xAxis.len, - (inverted ? chart.plotHeight : chart.plotWidth) - 2 * scrollbarHeight + (inverted ? chart.plotHeight : chart.plotWidth) - + 2 * scrollbarHeight ); if (inverted) { navigatorWidth = scrollbarHeight; } else { @@ -4552,26 +5219,44 @@ // Get the pixel position of the handles pxMin = pick(pxMin, xAxis.toPixels(min, true)); pxMax = pick(pxMax, xAxis.toPixels(max, true)); - if (!isNumber(pxMin) || Math.abs(pxMin) === Infinity) { // Verify (#1851, #2238) + // Verify (#1851, #2238) + if (!isNumber(pxMin) || Math.abs(pxMin) === Infinity) { pxMin = 0; pxMax = navigatorWidth; } // Are we below the minRange? (#2618, #6191) newMin = xAxis.toValue(pxMin, true); newMax = xAxis.toValue(pxMax, true); - if (Math.abs(newMax - newMin) < minRange) { + currentRange = Math.abs(H.correctFloat(newMax - newMin)); + if (currentRange < minRange) { if (this.grabbedLeft) { pxMin = xAxis.toPixels(newMax - minRange, true); } else if (this.grabbedRight) { pxMax = xAxis.toPixels(newMin + minRange, true); - } else { - return; } + } else if (defined(maxRange) && currentRange > maxRange) { + /** + * Maximum range which can be set using the navigator's handles. + * Opposite of [xAxis.minRange](#xAxis.minRange). + * + * @type {Number} + * @default undefined + * @product highstock + * @sample {highstock} stock/navigator/maxrange/ + * Defined max and min range + * @since 6.0.0 + * @apioption xAxis.maxRange + */ + if (this.grabbedLeft) { + pxMin = xAxis.toPixels(newMax - maxRange, true); + } else if (this.grabbedRight) { + pxMax = xAxis.toPixels(newMin + maxRange, true); + } } // Handles are allowed to cross, but never exceed the plot area navigator.zoomedMax = Math.min(Math.max(pxMin, pxMax, 0), zoomedMax); navigator.zoomedMin = Math.min( @@ -4596,12 +5281,15 @@ // Place elements verb = rendered && !navigator.hasDragged ? 'animate' : 'attr'; navigator.drawMasks(zoomedMin, zoomedMax, inverted, verb); navigator.drawOutline(zoomedMin, zoomedMax, inverted, verb); - navigator.drawHandle(zoomedMin, 0, inverted, verb); - navigator.drawHandle(zoomedMax, 1, inverted, verb); + + if (navigator.navigatorOptions.handles.enabled) { + navigator.drawHandle(zoomedMin, 0, inverted, verb); + navigator.drawHandle(zoomedMax, 1, inverted, verb); + } } if (navigator.scrollbar) { if (inverted) { scrollbarTop = navigator.top - scrollbarHeight; @@ -4625,11 +5313,12 @@ navigatorWidth, scrollbarHeight ); // Keep scale 0-1 navigator.scrollbar.setRange( - // Use real value, not rounded because range can be very small (#1716) + // Use real value, not rounded because range can be very small + // (#1716) navigator.zoomedMin / navigatorSize, navigator.zoomedMax / navigatorSize ); } navigator.rendered = true; @@ -4658,11 +5347,12 @@ }; // Add shades and handles mousedown events eventsToUnbind = navigator.getPartsEvents('mousedown'); // Add mouse move and mouseup events. These are bind to doc/container, - // because Navigator.grabbedSomething flags are stored in mousedown events: + // because Navigator.grabbedSomething flags are stored in mousedown + // events eventsToUnbind.push( addEvent(container, 'mousemove', mouseMoveHandler), addEvent(container.ownerDocument, 'mouseup', mouseUpHandler) ); @@ -4678,13 +5368,17 @@ navigator.eventsToUnbind = eventsToUnbind; // Data events if (navigator.series && navigator.series[0]) { eventsToUnbind.push( - addEvent(navigator.series[0].xAxis, 'foundExtremes', function() { - chart.navigator.modifyNavigatorAxisExtremes(); - }) + addEvent( + navigator.series[0].xAxis, + 'foundExtremes', + function() { + chart.navigator.modifyNavigatorAxisExtremes(); + } + ) ); } }, /** @@ -4711,11 +5405,11 @@ return events; }, /** * Mousedown on a shaded mask, either: - * - will be stored for future drag&drop + * - will be stored for future drag&drop * - will directly shift to a new range * * @param {Object} e Mouse event * @param {Number} index Index of a mask in Navigator.shades array */ @@ -4816,13 +5510,14 @@ dragOffset = navigator.dragOffset, inverted = chart.inverted, chartX; - // In iOS, a mousemove event with e.pageX === 0 is fired when holding the finger - // down in the center of the scrollbar. This should be ignored. - if (!e.touches || e.touches[0].pageX !== 0) { // #4696, scrollbar failed on Android + // In iOS, a mousemove event with e.pageX === 0 is fired when holding + // the finger down in the center of the scrollbar. This should be + // ignored. + if (!e.touches || e.touches[0].pageX !== 0) { // #4696 e = chart.pointer.normalize(e); chartX = e.chartX; // Swap some options for inverted chart @@ -4852,23 +5547,28 @@ // Drag scrollbar or open area in navigator } else if (navigator.grabbedCenter) { navigator.hasDragged = true; if (chartX < dragOffset) { // outside left chartX = dragOffset; - } else if (chartX > navigatorSize + dragOffset - range) { // outside right + // outside right + } else if (chartX > navigatorSize + dragOffset - range) { chartX = navigatorSize + dragOffset - range; } navigator.render( 0, 0, chartX - dragOffset, chartX - dragOffset + range ); } - if (navigator.hasDragged && navigator.scrollbar && navigator.scrollbar.options.liveRedraw) { - e.DOMType = e.type; // DOMType is for IE8 because it can't read type async + if ( + navigator.hasDragged && + navigator.scrollbar && + navigator.scrollbar.options.liveRedraw + ) { + e.DOMType = e.type; // DOMType is for IE8 setTimeout(function() { navigator.onMouseUp(e); }, 0); } } @@ -4915,12 +5615,13 @@ if (defined(ext.min)) { chart.xAxis[0].setExtremes( Math.min(ext.min, ext.max), Math.max(ext.min, ext.max), true, - navigator.hasDragged ? false : null, // Run animation when clicking buttons, scrollbar track etc, but not when dragging handles or scrollbar - { + // Run animation when clicking buttons, scrollbar track etc, + // but not when dragging handles or scrollbar + navigator.hasDragged ? false : null, { trigger: 'navigator', triggerOp: 'navigator-drag', DOMEvent: DOMEvent // #1838 } ); @@ -4960,11 +5661,15 @@ }, this); } // We only listen for extremes-events on the first baseSeries if (baseSeries[0].xAxis) { - removeEvent(baseSeries[0].xAxis, 'foundExtremes', this.modifyBaseAxisExtremes); + removeEvent( + baseSeries[0].xAxis, + 'foundExtremes', + this.modifyBaseAxisExtremes + ); } } }, /** @@ -4991,25 +5696,34 @@ this.navigatorEnabled = navigatorEnabled; this.navigatorOptions = navigatorOptions; this.scrollbarOptions = scrollbarOptions; this.outlineHeight = height + scrollbarHeight; - this.opposite = pick(navigatorOptions.opposite, !navigatorEnabled && chart.inverted); // #6262 + this.opposite = pick( + navigatorOptions.opposite, !navigatorEnabled && chart.inverted + ); // #6262 var navigator = this, baseSeries = navigator.baseSeries, xAxisIndex = chart.xAxis.length, yAxisIndex = chart.yAxis.length, - baseXaxis = baseSeries && baseSeries[0] && baseSeries[0].xAxis || chart.xAxis[0]; + baseXaxis = baseSeries && baseSeries[0] && baseSeries[0].xAxis || + chart.xAxis[0]; // Make room for the navigator, can be placed around the chart: chart.extraMargin = { type: navigator.opposite ? 'plotTop' : 'marginBottom', - value: (navigatorEnabled || !chart.inverted ? navigator.outlineHeight : 0) + navigatorOptions.margin + value: ( + navigatorEnabled || !chart.inverted ? + navigator.outlineHeight : + 0 + ) + navigatorOptions.margin }; if (chart.inverted) { - chart.extraMargin.type = navigator.opposite ? 'marginRight' : 'plotLeft'; + chart.extraMargin.type = navigator.opposite ? + 'marginRight' : + 'plotLeft'; } chart.isDirtyBox = true; if (navigator.navigatorEnabled) { // an x axis is required for scrollbar also @@ -5078,11 +5792,15 @@ translate: function(value, reverse) { var axis = chart.xAxis[0], ext = axis.getExtremes(), scrollTrackWidth = axis.len - 2 * scrollbarHeight, min = numExt('min', axis.options.min, ext.dataMin), - valueRange = numExt('max', axis.options.max, ext.dataMax) - min; + valueRange = numExt( + 'max', + axis.options.max, + ext.dataMax + ) - min; return reverse ? // from pixel to value (value * valueRange / scrollTrackWidth) + min : // from value to pixel @@ -5116,11 +5834,14 @@ from = range * this.from; navigator.hasDragged = navigator.scrollbar.hasDragged; navigator.render(0, 0, from, to); - if (chart.options.scrollbar.liveRedraw || e.DOMType !== 'mousemove') { + if ( + chart.options.scrollbar.liveRedraw || + e.DOMType !== 'mousemove' + ) { setTimeout(function() { navigator.onMouseUp(e); }); } }); @@ -5131,12 +5852,12 @@ // Add redraw events navigator.addChartEvents(); }, /** - * Get the union data extremes of the chart - the outer data extremes of the base - * X axis and the navigator axis. + * Get the union data extremes of the chart - the outer data extremes of the + * base X axis and the navigator axis. * @param {boolean} returnFalseOnNoBaseSeries - as the param says. */ getUnionExtremes: function(returnFalseOnNoBaseSeries) { var baseAxis = this.chart.xAxis[0], navAxis = this.xAxis, @@ -5170,24 +5891,34 @@ } return ret; }, /** - * Set the base series and update the navigator series from this. With a bit - * of modification we should be able to make this an API method to be called + * Set the base series and update the navigator series from this. With a bit + * of modification we should be able to make this an API method to be called * from the outside - * @param {Object} baseSeriesOptions - additional series options for a navigator + * @param {Object} baseSeriesOptions + * Additional series options for a navigator + * @param {Boolean} [redraw] + * Whether to redraw after update. */ - setBaseSeries: function(baseSeriesOptions) { + setBaseSeries: function(baseSeriesOptions, redraw) { var chart = this.chart, baseSeries = this.baseSeries = []; - baseSeriesOptions = baseSeriesOptions || chart.options && chart.options.navigator.baseSeries || 0; + baseSeriesOptions = ( + baseSeriesOptions || + chart.options && chart.options.navigator.baseSeries || + 0 + ); - // Iterate through series and add the ones that should be shown in navigator. + // Iterate through series and add the ones that should be shown in + // navigator. each(chart.series || [], function(series, i) { - if (!series.options.isInternal && // Don't include existing nav series + if ( + // Don't include existing nav series + !series.options.isInternal && ( series.options.showInNavigator || ( i === baseSeriesOptions || series.options.id === baseSeriesOptions @@ -5199,19 +5930,19 @@ } }); // When run after render, this.xAxis already exists if (this.xAxis && !this.xAxis.fake) { - this.updateNavigatorSeries(); + this.updateNavigatorSeries(redraw); } }, /* * Update series in the navigator from baseSeries, adding new if does not * exist. */ - updateNavigatorSeries: function() { + updateNavigatorSeries: function(redraw) { var navigator = this, chart = navigator.chart, baseSeries = navigator.baseSeries, baseOptions, mergedNavSeriesOptions, @@ -5234,12 +5965,12 @@ navigatorSeries = navigator.series = H.grep( navigator.series || [], function(navSeries) { var base = navSeries.baseSeries; if (H.inArray(base, baseSeries) < 0) { // Not in array - // If there is still a base series connected to this series, - // remove event handler and reference. + // If there is still a base series connected to this + // series, remove event handler and reference. if (base) { removeEvent( base, 'updatedData', navigator.updatedDataHandler @@ -5252,85 +5983,104 @@ } return true; } ); - // Go through each base series and merge the options to create new series + // Go through each base series and merge the options to create new + // series if (baseSeries && baseSeries.length) { - each(baseSeries, function(base, i) { + each(baseSeries, function eachBaseSeries(base) { var linkedNavSeries = base.navigatorSeries, - userNavOptions = !isArray(chartNavigatorSeriesOptions) ? - chartNavigatorSeriesOptions : {}; + userNavOptions = extend( + // Grab color from base as default + { + color: base.color + }, !isArray(chartNavigatorSeriesOptions) ? + chartNavigatorSeriesOptions : + defaultOptions.navigator.series + ); // Don't update if the series exists in nav and we have disabled // adaptToUpdatedData. if ( linkedNavSeries && navigator.navigatorOptions.adaptToUpdatedData === false ) { return; } - navSeriesMixin.name = 'Navigator ' + (i + 1); + navSeriesMixin.name = 'Navigator ' + baseSeries.length; baseOptions = base.options || {}; baseNavigatorOptions = baseOptions.navigatorOptions || {}; mergedNavSeriesOptions = merge( baseOptions, navSeriesMixin, userNavOptions, baseNavigatorOptions ); - // Merge data separately. Do a slice to avoid mutating the navigator options from base series (#4923). - var navigatorSeriesData = baseNavigatorOptions.data || userNavOptions.data; - navigator.hasNavigatorData = navigator.hasNavigatorData || !!navigatorSeriesData; - mergedNavSeriesOptions.data = navigatorSeriesData || baseOptions.data && baseOptions.data.slice(0); + // Merge data separately. Do a slice to avoid mutating the + // navigator options from base series (#4923). + var navigatorSeriesData = + baseNavigatorOptions.data || userNavOptions.data; + navigator.hasNavigatorData = + navigator.hasNavigatorData || !!navigatorSeriesData; + mergedNavSeriesOptions.data = + navigatorSeriesData || + baseOptions.data && baseOptions.data.slice(0); // Update or add the series - if (linkedNavSeries) { - linkedNavSeries.update(mergedNavSeriesOptions); + if (linkedNavSeries && linkedNavSeries.options) { + linkedNavSeries.update(mergedNavSeriesOptions, redraw); } else { - base.navigatorSeries = chart.initSeries(mergedNavSeriesOptions); + base.navigatorSeries = chart.initSeries( + mergedNavSeriesOptions + ); base.navigatorSeries.baseSeries = base; // Store ref navigatorSeries.push(base.navigatorSeries); } }); } - // If user has defined data (and no base series) or explicitly defined - // navigator.series as an array, we create these series on top of any + // If user has defined data (and no base series) or explicitly defined + // navigator.series as an array, we create these series on top of any // base series. if ( chartNavigatorSeriesOptions.data && !(baseSeries && baseSeries.length) || isArray(chartNavigatorSeriesOptions) ) { navigator.hasNavigatorData = false; // Allow navigator.series to be an array chartNavigatorSeriesOptions = H.splat(chartNavigatorSeriesOptions); each(chartNavigatorSeriesOptions, function(userSeriesOptions, i) { - mergedNavSeriesOptions = merge({ + navSeriesMixin.name = + 'Navigator ' + (navigatorSeries.length + 1); + mergedNavSeriesOptions = merge( + defaultOptions.navigator.series, { // Since we don't have a base series to pull color from, // try to fake it by using color from series with same - // index. Otherwise pull from the colors array. We need + // index. Otherwise pull from the colors array. We need // an explicit color as otherwise updates will increment // color counter and we'll get a new color for each // update of the nav series. color: chart.series[i] && !chart.series[i].options.isInternal && chart.series[i].color || chart.options.colors[i] || chart.options.colors[0] }, - userSeriesOptions, - navSeriesMixin + navSeriesMixin, + userSeriesOptions ); mergedNavSeriesOptions.data = userSeriesOptions.data; if (mergedNavSeriesOptions.data) { navigator.hasNavigatorData = true; - navigatorSeries.push(chart.initSeries(mergedNavSeriesOptions)); + navigatorSeries.push( + chart.initSeries(mergedNavSeriesOptions) + ); } }); } this.addBaseSeriesEvents(); @@ -5344,14 +6094,18 @@ var navigator = this, baseSeries = navigator.baseSeries || []; // Bind modified extremes event to first base's xAxis only. // In event of > 1 base-xAxes, the navigator will ignore those. - // Adding this multiple times to the same axis is no problem, as + // Adding this multiple times to the same axis is no problem, as // duplicates should be discarded by the browser. if (baseSeries[0] && baseSeries[0].xAxis) { - addEvent(baseSeries[0].xAxis, 'foundExtremes', this.modifyBaseAxisExtremes); + addEvent( + baseSeries[0].xAxis, + 'foundExtremes', + this.modifyBaseAxisExtremes + ); } each(baseSeries, function(base) { // Link base series show/hide to navigator series visibility addEvent(base, 'show', function() { @@ -5363,11 +6117,11 @@ if (this.navigatorSeries) { this.navigatorSeries.hide(); } }); - // Respond to updated data in the base series, unless explicitily + // Respond to updated data in the base series, unless explicitily // not adapting to data changes. if (this.navigatorOptions.adaptToUpdatedData !== false) { if (base.xAxis) { addEvent(base, 'updatedData', this.updatedDataHandler); } @@ -5383,21 +6137,27 @@ }); }, this); }, /** - * Set the navigator x axis extremes to reflect the total. The navigator extremes - * should always be the extremes of the union of all series in the chart as - * well as the navigator series. + * Set the navigator x axis extremes to reflect the total. The navigator + * extremes should always be the extremes of the union of all series in the + * chart as well as the navigator series. */ modifyNavigatorAxisExtremes: function() { var xAxis = this.xAxis, unionExtremes; if (xAxis.getExtremes) { unionExtremes = this.getUnionExtremes(true); - if (unionExtremes && (unionExtremes.dataMin !== xAxis.min || unionExtremes.dataMax !== xAxis.max)) { + if ( + unionExtremes && + ( + unionExtremes.dataMin !== xAxis.min || + unionExtremes.dataMax !== xAxis.max + ) + ) { xAxis.min = unionExtremes.dataMin; xAxis.max = unionExtremes.dataMax; } } }, @@ -5414,33 +6174,38 @@ baseDataMin = baseExtremes.dataMin, baseDataMax = baseExtremes.dataMax, range = baseMax - baseMin, stickToMin = navigator.stickToMin, stickToMax = navigator.stickToMax, + overscroll = baseXAxis.options.overscroll, newMax, newMin, navigatorSeries = navigator.series && navigator.series[0], hasSetExtremes = !!baseXAxis.setExtremes, - // When the extremes have been set by range selector button, don't stick to min or max. - // The range selector buttons will handle the extremes. (#5489) - unmutable = baseXAxis.eventArgs && baseXAxis.eventArgs.trigger === 'rangeSelectorButton'; + // When the extremes have been set by range selector button, don't + // stick to min or max. The range selector buttons will handle the + // extremes. (#5489) + unmutable = baseXAxis.eventArgs && + baseXAxis.eventArgs.trigger === 'rangeSelectorButton'; if (!unmutable) { - // If the zoomed range is already at the min, move it to the right as new data - // comes in + // If the zoomed range is already at the min, move it to the right + // as new data comes in if (stickToMin) { newMin = baseDataMin; newMax = newMin + range; } - // If the zoomed range is already at the max, move it to the right as new data - // comes in + // If the zoomed range is already at the max, move it to the right + // as new data comes in if (stickToMax) { - newMax = baseDataMax; - if (!stickToMin) { // if stickToMin is true, the new min value is set above + newMax = baseDataMax + overscroll; + + // if stickToMin is true, the new min value is set above + if (!stickToMin) { newMin = Math.max( newMax - range, navigatorSeries && navigatorSeries.xData ? navigatorSeries.xData[0] : -Number.MAX_VALUE ); @@ -5459,42 +6224,50 @@ // Reset navigator.stickToMin = navigator.stickToMax = null; }, /** - * Handler for updated data on the base series. When data is modified, the navigator series - * must reflect it. This is called from the Chart.redraw function before axis and series - * extremes are computed. + * Handler for updated data on the base series. When data is modified, the + * navigator series must reflect it. This is called from the Chart.redraw + * function before axis and series extremes are computed. */ updatedDataHandler: function() { var navigator = this.chart.navigator, baseSeries = this, navigatorSeries = this.navigatorSeries; - // If the scrollbar is scrolled all the way to the right, keep right as new data - // comes in. - navigator.stickToMax = Math.round(navigator.zoomedMax) >= Math.round(navigator.size); + // If the scrollbar is scrolled all the way to the right, keep right as + // new data comes in. + navigator.stickToMax = + Math.round(navigator.zoomedMax) >= Math.round(navigator.size); - // Detect whether the zoomed area should stick to the minimum or maximum. If the current - // axis minimum falls outside the new updated dataset, we must adjust. + // Detect whether the zoomed area should stick to the minimum or + // maximum. If the current axis minimum falls outside the new updated + // dataset, we must adjust. navigator.stickToMin = isNumber(baseSeries.xAxis.min) && (baseSeries.xAxis.min <= baseSeries.xData[0]) && (!this.chart.fixedRange || !navigator.stickToMax); // Set the navigator series data to the new data of the base series if (navigatorSeries && !navigator.hasNavigatorData) { navigatorSeries.options.pointStart = baseSeries.xData[0]; - navigatorSeries.setData(baseSeries.options.data, false, null, false); // #5414 + navigatorSeries.setData( + baseSeries.options.data, + false, + null, + false + ); // #5414 } }, /** * Add chart events, like redrawing navigator, when chart requires that. */ addChartEvents: function() { addEvent(this.chart, 'redraw', function() { - // Move the scrollbar after redraw, like after data updata even if axes don't redraw + // Move the scrollbar after redraw, like after data updata even if + // axes don't redraw var navigator = this.navigator, xAxis = navigator && ( navigator.baseSeries && navigator.baseSeries[0] && navigator.baseSeries[0].xAxis || @@ -5550,12 +6323,13 @@ }; H.Navigator = Navigator; /** - * For Stock charts, override selection zooming with some special features because - * X axis zooming is already allowed by the Navigator and Range selector. + * For Stock charts, override selection zooming with some special features + * because X axis zooming is already allowed by the Navigator and Range + * selector. */ wrap(Axis.prototype, 'zoom', function(proceed, newMin, newMax) { var chart = this.chart, chartOptions = chart.options, zoomType = chartOptions.chart.zoomType, @@ -5565,21 +6339,21 @@ ret; if (this.isXAxis && ((navigator && navigator.enabled) || (rangeSelector && rangeSelector.enabled))) { - // For x only zooming, fool the chart.zoom method not to create the zoom button - // because the property already exists + // For x only zooming, fool the chart.zoom method not to create the zoom + // button because the property already exists if (zoomType === 'x') { chart.resetZoomButton = 'blocked'; // For y only zooming, ignore the X axis completely } else if (zoomType === 'y') { ret = false; - // For xy zooming, record the state of the zoom before zoom selection, then when - // the reset button is pressed, revert to this state + // For xy zooming, record the state of the zoom before zoom selection, + // then when the reset button is pressed, revert to this state } else if (zoomType === 'xy') { previousZoom = this.previousZoom; if (defined(newMin)) { this.previousZoom = [this.min, this.max]; } else if (previousZoom) { @@ -5606,13 +6380,14 @@ proceed.call(this, options, callback); }); /** - * For stock charts, extend the Chart.setChartSize method so that we can set the final top position - * of the navigator once the height of the chart, including the legend, is determined. #367. - * We can't use Chart.getMargins, because labels offsets are not calculated yet. + * For stock charts, extend the Chart.setChartSize method so that we can set the + * final top position of the navigator once the height of the chart, including + * the legend, is determined. #367. We can't use Chart.getMargins, because + * labels offsets are not calculated yet. */ wrap(Chart.prototype, 'setChartSize', function(proceed) { var legend = this.legend, navigator = this.navigator, @@ -5622,11 +6397,11 @@ yAxis; proceed.apply(this, [].slice.call(arguments, 1)); if (navigator) { - legendOptions = legend.options; + legendOptions = legend && legend.options; xAxis = navigator.xAxis; yAxis = navigator.yAxis; scrollbarHeight = navigator.scrollbarHeight; // Compute the top position @@ -5636,13 +6411,29 @@ this.spacing[3] + scrollbarHeight; navigator.top = this.plotTop + scrollbarHeight; } else { navigator.left = this.plotLeft + scrollbarHeight; navigator.top = navigator.navigatorOptions.top || - this.chartHeight - navigator.height - scrollbarHeight - this.spacing[2] - - (legendOptions.verticalAlign === 'bottom' && legendOptions.enabled && !legendOptions.floating ? - legend.legendHeight + pick(legendOptions.margin, 10) : 0); + this.chartHeight - + navigator.height - + scrollbarHeight - + this.spacing[2] - + ( + this.rangeSelector && this.extraBottomMargin ? + this.rangeSelector.getHeight() : + 0 + ) - + ( + ( + legendOptions && + legendOptions.verticalAlign === 'bottom' && + legendOptions.enabled && + !legendOptions.floating + ) ? + legend.legendHeight + pick(legendOptions.margin, 10) : + 0 + ); } if (xAxis && yAxis) { // false if navigator is disabled (#904) if (this.inverted) { @@ -5656,23 +6447,40 @@ } } }); // Pick up badly formatted point options to addPoint - wrap(Series.prototype, 'addPoint', function(proceed, options, redraw, shift, animation) { + wrap(Series.prototype, 'addPoint', function( + proceed, + options, + redraw, + shift, + animation + ) { var turboThreshold = this.options.turboThreshold; - if (turboThreshold && this.xData.length > turboThreshold && isObject(options, true) && this.chart.navigator) { + if ( + turboThreshold && + this.xData.length > turboThreshold && + isObject(options, true) && + this.chart.navigator + ) { error(20, true); } proceed.call(this, options, redraw, shift, animation); }); // Handle adding new series - wrap(Chart.prototype, 'addSeries', function(proceed, options, redraw, animation) { + wrap(Chart.prototype, 'addSeries', function( + proceed, + options, + redraw, + animation + ) { var series = proceed.call(this, options, false, animation); if (this.navigator) { - this.navigator.setBaseSeries(); // Recompute which series should be shown in navigator, and add them + // Recompute which series should be shown in navigator, and add them + this.navigator.setBaseSeries(null, false); } if (pick(redraw, true)) { this.redraw(); } return series; @@ -5680,11 +6488,11 @@ // Handle updating series wrap(Series.prototype, 'update', function(proceed, newOptions, redraw) { proceed.call(this, newOptions, false); if (this.chart.navigator && !this.options.isInternal) { - this.chart.navigator.setBaseSeries(); + this.chart.navigator.setBaseSeries(null, false); } if (pick(redraw, true)) { this.chart.redraw(); } }); @@ -5698,13 +6506,10 @@ extremes = chart.xAxis[0].getExtremes(); navigator.render(extremes.min, extremes.max); } }); - /* **************************************************************************** - * End Navigator code * - *****************************************************************************/ }(Highcharts)); (function(H) { /** * (c) 2010-2017 Torstein Honsi @@ -5742,86 +6547,147 @@ * The range selector is a tool for selecting ranges to display within * the chart. It provides buttons to select preconfigured ranges in * the chart, like 1 day, 1 week, 1 month etc. It also provides input * boxes where min and max dates can be manually input. * - * @optionparent rangeSelector * @product highstock + * @optionparent rangeSelector */ rangeSelector: { // allButtonsEnabled: false, // enabled: true, // buttons: {Object} // buttonSpacing: 0, /** + * The vertical alignment of the rangeselector box. Allowed properties are `top`, + * `middle`, `bottom`. + * + * @since 6.0.0 + * + * @sample {highstock} stock/rangeselector/vertical-align-middle/ Middle + * + * @sample {highstock} stock/rangeselector/vertical-align-bottom/ Bottom + */ + verticalAlign: 'top', + + /** * A collection of attributes for the buttons. The object takes SVG * attributes like `fill`, `stroke`, `stroke-width`, as well as `style`, * a collection of CSS properties for the text. * * The object can also be extended with states, so you can set presentational * options for `hover`, `select` or `disabled` button states. * * CSS styles for the text label. * - * In [styled mode](http://www.highcharts.com/docs/chart-design-and- - * style/style-by-css), the buttons are styled by the `.highcharts- + * In styled mode, the buttons are styled by the `.highcharts- * range-selector-buttons .highcharts-button` rule with its different * states. * * @type {Object} * @sample {highstock} stock/rangeselector/styling/ Styling the buttons and inputs * @product highstock */ buttonTheme: { - - /** - */ 'stroke-width': 0, - - /** - */ width: 28, - - /** - */ height: 18, - - /** - */ padding: 2, - - /** - */ zIndex: 7 // #484, #852 }, /** - * The height of the range selector, used to reserve space for buttons - * and input. + * When the rangeselector is floating, the plot area does not reserve + * space for it. This opens for positioning anywhere on the chart. * + * @sample {highstock} stock/rangeselector/floating/ + * Placing the range selector between the plot area and the + * navigator + * @since 6.0.0 + * @product highstock + */ + floating: false, + + /** + * The x offset of the range selector relative to its horizontal + * alignment within `chart.spacingLeft` and `chart.spacingRight`. + * + * @since 6.0.0 + * @product highstock + */ + x: 0, + + /** + * The y offset of the range selector relative to its horizontal + * alignment within `chart.spacingLeft` and `chart.spacingRight`. + * + * @since 6.0.0 + * @product highstock + */ + y: 0, + + /** + * Deprecated. The height of the range selector. Currently it is + * calculated dynamically. + * * @type {Number} - * @default 35 + * @default undefined * @since 2.1.9 * @product highstock + * @deprecated true */ - height: 35, // reserved space for buttons and input + height: undefined, // reserved space for buttons and input /** * Positioning for the input boxes. Allowed properties are `align`, - * `verticalAlign`, `x` and `y`. + * `x` and `y`. * * @type {Object} * @default { align: "right" } - * @since 1.2.5 + * @since 1.2.4 * @product highstock */ inputPosition: { + /** + * The alignment of the input box. Allowed properties are `left`, + * `center`, `right`. + * @validvalue ["left", "center", "right"] + * @sample {highstock} stock/rangeselector/input-button-position/ + * Alignment + * @since 6.0.0 + */ + align: 'right', + x: 0, + y: 0 + }, + /** + * Positioning for the button row. + * + * @since 1.2.4 + * @product highstock + */ + buttonPosition: { /** + * The alignment of the input box. Allowed properties are `left`, + * `center`, `right`. + * + * @validvalue ["left", "center", "right"] + * @sample {highstock} stock/rangeselector/input-button-position/ + * Alignment + * @since 6.0.0 */ - align: 'right' + align: 'left', + /** + * X offset of the button row. + */ + x: 0, + /** + * Y offset of the button row. + */ + y: 0 }, // inputDateFormat: '%b %e, %Y', // inputEditDateFormat: '%Y-%m-%d', // inputEnabled: true, // selected: undefined, @@ -5829,22 +6695,17 @@ // inputStyle: {}, /** * CSS styles for the labels - the Zoom, From and To texts. * - * In [styled mode](http://www.highcharts.com/docs/chart-design-and- - * style/style-by-css), the labels are styled by the `.highcharts- - * range-label` class. + * In styled mode, the labels are styled by the `.highcharts-range-label` class. * * @type {CSSObject} * @sample {highstock} stock/rangeselector/styling/ Styling the buttons and inputs * @product highstock */ labelStyle: { - - /** - */ color: '#666666' } } }); @@ -5855,17 +6716,21 @@ * Language object. The language object is global and it can't be set * on each chart initiation. Instead, use `Highcharts.setOptions` to * set it before any chart is initialized. * * <pre>Highcharts.setOptions({ - * lang: { - * months: ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', - * 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'], - * - * weekdays: ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', - * 'Samedi'] - * } + * lang: { + * months: [ + * 'Janvier', 'Février', 'Mars', 'Avril', + * 'Mai', 'Juin', 'Juillet', 'Août', + * 'Septembre', 'Octobre', 'Novembre', 'Décembre' + * ], + * weekdays: [ + * 'Dimanche', 'Lundi', 'Mardi', 'Mercredi', + * 'Jeudi', 'Vendredi', 'Samedi' + * ] + * } * });</pre> * * @optionparent lang * @product highstock */ @@ -6011,10 +6876,14 @@ } } else if (type === 'all' && baseAxis) { newMin = dataMin; newMax = dataMax; } + + newMin += rangeOptions._offsetMin; + newMax += rangeOptions._offsetMax; + rangeSelector.setSelected(i); // Update the chart if (!baseAxis) { // Axis not yet instanciated. Temporarily set min and range @@ -6087,15 +6956,15 @@ buttonOptions = options.buttons || [].concat(rangeSelector.defaultButtons), selectedOption = options.selected, blurInputs = function() { var minInput = rangeSelector.minInput, maxInput = rangeSelector.maxInput; - if (minInput && minInput.blur) { //#3274 in some case blur is not defined - fireEvent(minInput, 'blur'); //#3274 + if (minInput && minInput.blur) { // #3274 in some case blur is not defined + fireEvent(minInput, 'blur'); // #3274 } - if (maxInput && maxInput.blur) { //#3274 in some case blur is not defined - fireEvent(maxInput, 'blur'); //#3274 + if (maxInput && maxInput.blur) { // #3274 in some case blur is not defined + fireEvent(maxInput, 'blur'); // #3274 } }; rangeSelector.chart = chart; rangeSelector.options = options; @@ -6154,10 +7023,11 @@ count = rangeOptions.count || 1, button = buttons[i], state = 0, disable, select, + offsetRange = rangeOptions._offsetMax - rangeOptions._offsetMin, isSelected = i === selected, // Disable buttons where the range exceeds what is allowed in the current view isTooGreatRange = range > dataMax - dataMin, // Disable buttons where the range is smaller than the minimum range isTooSmallRange = range < baseAxis.minRange, @@ -6170,19 +7040,19 @@ if ( (type === 'month' || type === 'year') && (actualRange >= { month: 28, year: 365 - }[type] * day * count) && + }[type] * day * count + offsetRange) && (actualRange <= { month: 31, year: 366 - }[type] * day * count) + }[type] * day * count + offsetRange) ) { isSameRange = true; } else if (type === 'ytd') { - isSameRange = (ytdMax - ytdMin) === actualRange; + isSameRange = (ytdMax - ytdMin + offsetRange) === actualRange; isYTDButNotSelected = !isSelected; } else if (type === 'all') { isSameRange = baseAxis.max - baseAxis.min >= dataMax - dataMin; isAllButAlreadyShowingAll = !isSelected && selectedExists && isSameRange; } @@ -6241,10 +7111,14 @@ rangeOptions._range = { month: 30, year: 365 }[type] * 24 * 36e5 * count; } + + rangeOptions._offsetMin = pick(rangeOptions.offsetMin, 0); + rangeOptions._offsetMax = pick(rangeOptions.offsetMax, 0); + rangeOptions._range += rangeOptions._offsetMax - rangeOptions._offsetMin; }, /** * Set the internal and displayed value of a HTML input for the dates * @param {String} name @@ -6446,15 +7320,15 @@ * Get the position of the range selector buttons and inputs. This can be overridden from outside for custom positioning. */ getPosition: function() { var chart = this.chart, options = chart.options.rangeSelector, - buttonTop = pick((options.buttonPosition || {}).y, chart.plotTop - chart.axisOffset[0] - options.height); + top = (options.verticalAlign) === 'top' ? chart.plotTop - chart.axisOffset[0] : 0; // set offset only for varticalAlign top return { - buttonTop: buttonTop, - inputTop: buttonTop - 10 + buttonTop: top + options.buttonPosition.y, + inputTop: top + options.inputPosition.y - 10 }; }, /** * Get the extremes of YTD. * Will choose dataMax if its value is lower than the current timestamp. @@ -6491,49 +7365,83 @@ renderer = chart.renderer, container = chart.container, chartOptions = chart.options, navButtonOptions = chartOptions.exporting && chartOptions.exporting.enabled !== false && chartOptions.navigation && chartOptions.navigation.buttonOptions, - options = chartOptions.rangeSelector, - buttons = rangeSelector.buttons, lang = defaultOptions.lang, div = rangeSelector.div, + options = chartOptions.rangeSelector, + floating = options.floating, + buttons = rangeSelector.buttons, inputGroup = rangeSelector.inputGroup, buttonTheme = options.buttonTheme, - buttonPosition = options.buttonPosition || {}, + buttonPosition = options.buttonPosition, + inputPosition = options.inputPosition, inputEnabled = options.inputEnabled, states = buttonTheme && buttonTheme.states, plotLeft = chart.plotLeft, buttonLeft, pos = this.getPosition(), - buttonGroup = rangeSelector.group, - buttonBBox, - rendered = rangeSelector.rendered; + buttonGroup = rangeSelector.buttonGroup, + group, + groupHeight, + rendered = rangeSelector.rendered, + verticalAlign = rangeSelector.options.verticalAlign, + legend = chart.legend, + legendOptions = legend && legend.options, + buttonPositionY = buttonPosition.y, + inputPositionY = inputPosition.y, + exportingX = 0, + alignTranslateY, + legendHeight, + minPosition, + translateY, + translateX, + groupOffsetY; if (options.enabled === false) { return; } // create the elements if (!rendered) { - rangeSelector.group = buttonGroup = renderer.g('range-selector-buttons').add(); + rangeSelector.group = group = renderer.g('range-selector-group') + .attr({ + zIndex: 7 + }) + .add(); - rangeSelector.zoomText = renderer.text(lang.rangeSelectorZoom, pick(buttonPosition.x, plotLeft), 15) + rangeSelector.buttonGroup = buttonGroup = renderer.g('range-selector-buttons').add(group); + + rangeSelector.zoomText = renderer.text(lang.rangeSelectorZoom, pick(plotLeft + buttonPosition.x, plotLeft), 15) .css(options.labelStyle) .add(buttonGroup); - // button starting position - buttonLeft = pick(buttonPosition.x, plotLeft) + rangeSelector.zoomText.getBBox().width + 5; + // button start position + buttonLeft = pick(plotLeft + buttonPosition.x, plotLeft) + rangeSelector.zoomText.getBBox().width + 5; each(rangeSelector.buttonOptions, function(rangeOptions, i) { + buttons[i] = renderer.button( rangeOptions.text, buttonLeft, 0, function() { - rangeSelector.clickButton(i); + + // extract events from button object and call + var buttonEvents = rangeOptions.events && rangeOptions.events.click, + callDefaultEvent; + + if (buttonEvents) { + callDefaultEvent = buttonEvents.call(rangeOptions); + } + + if (callDefaultEvent !== false) { + rangeSelector.clickButton(i); + } + rangeSelector.isActive = true; }, buttonTheme, states && states.hover, states && states.select, @@ -6559,57 +7467,267 @@ container.parentNode.insertBefore(div, container); // Create the group to keep the inputs rangeSelector.inputGroup = inputGroup = renderer.g('input-group') - .add(); + .add(group); inputGroup.offset = 0; rangeSelector.drawInput('min'); rangeSelector.drawInput('max'); } } + + plotLeft = chart.plotLeft - chart.spacing[3]; + rangeSelector.updateButtonStates(); - // Set or update the group position - buttonGroup[rendered ? 'animate' : 'attr']({ - translateY: pos.buttonTop + // detect collisiton with exporting + if ( + navButtonOptions && + this.titleCollision(chart) && + verticalAlign === 'top' && + buttonPosition.align === 'right' && + ( + (buttonPosition.y + buttonGroup.getBBox().height - 12) < + ((navButtonOptions.y || 0) + navButtonOptions.height - chart.spacing[0]) + ) + ) { + exportingX = -40; + } + + // align button group + buttonGroup.align(extend({ + y: pos.buttonTop, + width: buttonGroup.getBBox().width, + x: exportingX + }, buttonPosition), true, chart.spacingBox); + + translateX = buttonGroup.alignAttr.translateX + exportingX; + + // detect left offset (axis title) or margin + if (buttonPosition.align === 'left') { + translateX += ((plotLeft < 0) || (H.isNumber(chart.margin[3])) ? 0 : plotLeft) - chart.spacing[3]; + } else if (buttonPosition.align === 'right') { + translateX -= chart.spacing[1] + (H.isNumber(chart.margin[3]) ? plotLeft : 0); + } + + // Set / update the group position + buttonGroup.attr({ + translateY: pos.buttonTop, + translateX: translateX }); + // skip animation + rangeSelector.group.placed = false; + rangeSelector.buttonGroup.placed = false; + if (inputEnabled !== false) { + var inputGroupX, + inputGroupWidth, + buttonGroupX, + buttonGroupWidth; + + // detect collision with exporting + if ( + navButtonOptions && + this.titleCollision(chart) && + verticalAlign === 'top' && + inputPosition.align === 'right' && + ( + (pos.inputTop - inputGroup.getBBox().height - 12) < + ((navButtonOptions.y || 0) + navButtonOptions.height + chart.spacing[0]) + ) + ) { + exportingX = -40; + } else { + exportingX = 0; + } + // Update the alignment to the updated spacing box inputGroup.align(extend({ y: pos.inputTop, - width: inputGroup.offset, - // Detect collision with the exporting buttons - x: navButtonOptions && (pos.inputTop < (navButtonOptions.y || 0) + navButtonOptions.height - chart.spacing[0]) ? - -40 : 0 - }, options.inputPosition), true, chart.spacingBox); + width: inputGroup.getBBox().width + }, inputPosition), true, chart.spacingBox); - // Hide if overlapping - inputEnabled is null or undefined - if (!defined(inputEnabled)) { - buttonBBox = buttonGroup.getBBox(); - inputGroup[inputGroup.alignAttr.translateX < buttonBBox.x + buttonBBox.width + 10 ? 'hide' : 'show'](); + translateX = inputGroup.alignAttr.translateX + exportingX; + + if (inputPosition.align === 'left') { + translateX += plotLeft; + } else if ( + inputPosition.align === 'right' + ) { + translateX = translateX - chart.axisOffset[1]; // yAxis offset } + // add y from user options + inputGroup.attr({ + translateY: pos.inputTop + 10, + translateX: translateX - (inputPosition.align === 'right' ? 2 : 0) // fix wrong getBBox() value on right align + }); + + // detect collision + inputGroupX = inputGroup.translateX + inputGroup.alignOptions.x - + exportingX + inputGroup.getBBox().x + 2; // getBBox for detecing left margin, 2px padding to not overlap input and label + + inputGroupWidth = inputGroup.alignOptions.width; + + buttonGroupX = buttonGroup.translateX + buttonGroup.getBBox().x; + buttonGroupWidth = buttonGroup.getBBox().width + 20; // 20 is minimal spacing between elements + + if ( + (inputPosition.align === buttonPosition.align) || + ( + (buttonGroupX + buttonGroupWidth > inputGroupX) && + (inputGroupX + inputGroupWidth > buttonGroupX) && + (buttonPositionY < (inputPositionY + inputGroup.getBBox().height)) + ) + ) { + + // move the element to the second line + inputGroup.attr({ + translateX: inputGroup.translateX, + translateY: inputGroup.translateY + buttonGroup.getBBox().height + 10 + }); + } + // Set or reset the input values rangeSelector.setInputValue('min', min); rangeSelector.setInputValue('max', max); + + // skip animation + rangeSelector.inputGroup.placed = false; } + // vertical align + rangeSelector.group.align({ + verticalAlign: verticalAlign + }, true, chart.spacingBox); + + // set position + groupHeight = rangeSelector.group.getBBox().height + 20; // # 20 padding + + // calculate bottom position + if (verticalAlign === 'bottom') { + legendHeight = legendOptions && legendOptions.verticalAlign === 'bottom' && legendOptions.enabled && + !legendOptions.floating ? legend.legendHeight + pick(legendOptions.margin, 10) : 0; + + groupHeight = groupHeight + legendHeight - 20; + } + + groupOffsetY = Math[verticalAlign === 'middle' ? 'max' : 'min'](inputPositionY, buttonPositionY); + + if (inputGroup && (inputPositionY < buttonPositionY) && verticalAlign === 'bottom') { + groupOffsetY += inputGroup.getBBox().height; + } + + // fix the position + alignTranslateY = rangeSelector.group.alignAttr.translateY; + minPosition = (inputPositionY < 0 && buttonPositionY < 0) ? 0 : groupOffsetY; + translateY = Math.floor(alignTranslateY - groupHeight - minPosition); + + if (verticalAlign === 'top') { + if (floating) { + translateY = 0; + } else if (chart.spacing[0] !== chart.options.chart.spacing[0]) { // detect if spacing is customised + translateY -= (chart.spacing[0] - chart.options.chart.spacing[0]); + } + } else if (verticalAlign === 'middle') { + if (inputPositionY === buttonPositionY) { + if (inputPositionY < 0) { + translateY = alignTranslateY + minPosition; + } else { + translateY = alignTranslateY; + } + } else if (inputPositionY || buttonPositionY) { + if (inputPositionY < 0 || buttonPositionY < 0) { + translateY -= Math.min(inputPositionY, buttonPositionY); + } else { + translateY = alignTranslateY - groupHeight + minPosition; + } + } + } + + translateY = Math.floor(translateY); + + if (floating) { + translateY += options.y; + } + + rangeSelector.group.translate(0 + options.x, translateY - 3); // floor to avoid crisp edges, 3px to keep back compatibility + + // translate HTML inputs + if (inputEnabled !== false) { + rangeSelector.minInput.style.marginTop = rangeSelector.group.translateY + 'px'; + rangeSelector.maxInput.style.marginTop = rangeSelector.group.translateY + 'px'; + } + rangeSelector.rendered = true; }, + /** + * Extracts height of range selector + * @return {Number} Returns rangeSelector height + */ + getHeight: function() { + var rangeSelector = this, + options = rangeSelector.options, + inputPosition = options.inputPosition, + buttonPosition = options.buttonPosition, + yPosition = options.y, + rangeSelectorGroup = rangeSelector.group, + buttonPositionY = buttonPosition.y, + inputPositionY = inputPosition.y, + rangeSelectorHeight = 0, + minPosition; + + rangeSelectorHeight = rangeSelectorGroup ? (rangeSelectorGroup.getBBox(true).height) + 13 + yPosition : 0; // 13px to keep back compatibility + minPosition = Math.min(inputPositionY, buttonPositionY); + + if ( + (inputPositionY < 0 && buttonPositionY < 0) || + (inputPositionY > 0 && buttonPositionY > 0) + ) { + rangeSelectorHeight += Math.abs(minPosition); + } + + return rangeSelectorHeight; + }, + /** + * Detect collision with title or subtitle + * @param {object} chart + * @return {Boolean} Returns collision status + */ + titleCollision: function(chart) { + var status = false; + + if ( + (!H.isObject(chart.title) || + (chart.title && chart.title.getBBox().y > chart.plotTop) + ) && (!H.isObject(chart.subtitle) || + (chart.subtitle && chart.subtitle.getBBox().y > chart.plotTop) + ) + ) { + status = true; + } + + return status; + }, + + /** * Update the range selector with new options + * @param {object} options */ update: function(options) { var chart = this.chart; + merge(true, chart.options.rangeSelector, options); this.destroy(); this.init(chart); + chart.rangeSelector.render(); }, /** * Destroys allocated elements. */ @@ -6738,11 +7856,11 @@ } return min; }; - // Initialize scroller for stock charts + // Initialize rangeselector for stock charts wrap(Chart.prototype, 'init', function(proceed, options, callback) { addEvent(this, 'init', function() { if (this.options.rangeSelector.enabled) { this.rangeSelector = new RangeSelector(this); @@ -6751,10 +7869,107 @@ proceed.call(this, options, callback); }); + wrap(Chart.prototype, 'render', function(proceed, options, callback) { + + var chart = this, + rangeSelector = chart.rangeSelector, + verticalAlign; + + if (rangeSelector) { + + rangeSelector.render(); + verticalAlign = rangeSelector.options.verticalAlign; + + if (!rangeSelector.options.floating) { + if (verticalAlign === 'bottom') { + this.extraBottomMargin = true; + } else if (verticalAlign !== 'middle') { + this.extraTopMargin = true; + } + } + } + + proceed.call(this, options, callback); + + }); + + wrap(Chart.prototype, 'update', function(proceed, options, redraw, oneToOne) { + + var chart = this, + rangeSelector = chart.rangeSelector, + verticalAlign; + + this.extraBottomMargin = false; + this.extraTopMargin = false; + + if (rangeSelector) { + + rangeSelector.render(); + + verticalAlign = (options.rangeSelector && options.rangeSelector.verticalAlign) || + (rangeSelector.options && rangeSelector.options.verticalAlign); + + if (!rangeSelector.options.floating) { + if (verticalAlign === 'bottom') { + this.extraBottomMargin = true; + } else if (verticalAlign !== 'middle') { + this.extraTopMargin = true; + } + } + } + + proceed.call(this, H.merge(true, options, { + chart: { + marginBottom: pick(options.chart && options.chart.marginBottom, chart.margin.bottom), + spacingBottom: pick(options.chart && options.chart.spacingBottom, chart.spacing.bottom) + } + }), redraw, oneToOne); + + }); + + wrap(Chart.prototype, 'redraw', function(proceed, options, callback) { + var chart = this, + rangeSelector = chart.rangeSelector, + verticalAlign; + + if (rangeSelector && !rangeSelector.options.floating) { + + rangeSelector.render(); + verticalAlign = rangeSelector.options.verticalAlign; + + if (verticalAlign === 'bottom') { + this.extraBottomMargin = true; + } else if (verticalAlign !== 'middle') { + this.extraTopMargin = true; + } + } + + proceed.call(this, options, callback); + }); + + Chart.prototype.adjustPlotArea = function() { + var chart = this, + rangeSelector = chart.rangeSelector, + rangeSelectorHeight; + + if (this.rangeSelector) { + + rangeSelectorHeight = rangeSelector.getHeight(); + + if (this.extraTopMargin) { + this.plotTop += rangeSelectorHeight; + } + + if (this.extraBottomMargin) { + this.marginBottom += rangeSelectorHeight; + } + } + }; + Chart.prototype.callbacks.push(function(chart) { var extremes, rangeSelector = chart.rangeSelector, unbindRender, unbindSetExtremes; @@ -6794,11 +8009,11 @@ H.RangeSelector = RangeSelector; /* **************************************************************************** - * End Range Selector code * + * End Range Selector code * *****************************************************************************/ }(Highcharts)); (function(H) { /** @@ -6833,11 +8048,58 @@ 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. + * + * @type {String} + * @see [compareBase](#plotOptions.series.compareBase), [Axis.setCompare()](#Axis. + * setCompare()) + * @sample {highstock} stock/plotoptions/series-compare-percent/ Percent + * @sample {highstock} stock/plotoptions/series-compare-value/ Value + * @default undefined + * @since 1.0.1 + * @product highstock + * @apioption plotOptions.series.compare + */ + + /** + * Defines if comparisson should start from the first point within the visible + * range or should start from the first point <b>before</b> the range. + * In other words, this flag determines if first point within the visible range + * will have 0% (base) or should have been already calculated according to the + * previous point. + * + * @type {Boolean} + * @sample {highstock} stock/plotoptions/series-comparestart/ Calculate compare within visible range + * @default undefined + * @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. + * + * @validvalue [0, 100] + * @type {Number} + * @sample {highstock} / Compare base is 100 + * @default 0 + * @since 5.0.6 + * @product highstock + * @apioption plotOptions.series.compareBase + */ + + /** * Factory function for creating new stock charts. Creates a new {@link Chart| * Chart} object with different default options than the basic Chart. * * @function #stockChart * @memberOf Highcharts @@ -6903,10 +8165,11 @@ // apply X axis options to both single and multi y axes options.xAxis = map(splat(options.xAxis || {}), function(xAxisOptions) { return merge({ // defaults minPadding: 0, maxPadding: 0, + overscroll: 0, ordinal: true, title: { text: null }, labels: { @@ -6962,11 +8225,11 @@ }, title: { text: null }, tooltip: { - shared: true, + split: true, crosshairs: true }, legend: { enabled: false }, @@ -7047,11 +8310,11 @@ x1, y1, x2, y2, result = [], - axes = [], //#3416 need a default array + axes = [], // #3416 need a default array axes2, uniqueAxes, transVal; /** @@ -7100,11 +8363,11 @@ // 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 + uniqueAxes = axes.length ? [] : [axis.isXAxis ? chart.yAxis[0] : chart.xAxis[0]]; // #3742 each(axes, function(axis2) { if ( inArray(axis2, uniqueAxes) === -1 && // Do not draw on axis which overlap completely. #5424 !H.find(uniqueAxes, function(unique) { @@ -7157,11 +8420,11 @@ }); } } return result.length > 0 ? renderer.crispPolyLine(result, lineWidth || 1) : - null; //#3557 getPlotLinePath in regular Highcharts also returns null + null; // #3557 getPlotLinePath in regular Highcharts also returns null }); // Override getPlotBandPath to allow for multipane charts Axis.prototype.getPlotBandPath = function(from, to) { var toPath = this.getPlotLinePath(to, null, null, true), @@ -7440,10 +8703,11 @@ var series = this, i, keyIndex = -1, processedXData, processedYData, + compareStart = series.options.compareStart === true ? 0 : 1, length, compareValue; // call base method seriesProcessData.apply(this, arguments); @@ -7464,14 +8728,18 @@ keyIndex = inArray(series.pointValKey || 'y', series.pointArrayMap); } } // find the first value for comparison - for (i = 0; i < length - 1; i++) { + for (i = 0; i < length - compareStart; i++) { compareValue = processedYData[i] && keyIndex > -1 ? processedYData[i][keyIndex] : processedYData[i]; - if (isNumber(compareValue) && processedXData[i + 1] >= series.xAxis.min && compareValue !== 0) { + if ( + isNumber(compareValue) && + processedXData[i + compareStart] >= series.xAxis.min && + compareValue !== 0 + ) { series.compareValue = compareValue; break; } } }